nico-tools 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/bin/cli.js +9 -13
  2. package/bin/commands/create-project.js +106 -0
  3. package/bin/commands/translate.js +12 -0
  4. package/bin/config/templates.js +9 -0
  5. package/bin/utils/file-utils.js +84 -0
  6. package/bin/utils/project-utils.js +49 -0
  7. package/package.json +4 -2
  8. package/templates/react-native/expo-clean-architecture/README.md +154 -0
  9. package/templates/react-native/expo-clean-architecture/app.config.ts +61 -0
  10. package/templates/react-native/expo-clean-architecture/assets/config/.gitkeep +3 -0
  11. package/templates/react-native/expo-clean-architecture/assets/fonts/SpaceMono-Regular.ttf +0 -0
  12. package/templates/react-native/expo-clean-architecture/assets/images/adaptive-icon.png +0 -0
  13. package/templates/react-native/expo-clean-architecture/assets/images/favicon.png +0 -0
  14. package/templates/react-native/expo-clean-architecture/assets/images/icon.png +0 -0
  15. package/templates/react-native/expo-clean-architecture/assets/images/partial-react-logo.png +0 -0
  16. package/templates/react-native/expo-clean-architecture/assets/images/react-logo.png +0 -0
  17. package/templates/react-native/expo-clean-architecture/assets/images/react-logo@2x.png +0 -0
  18. package/templates/react-native/expo-clean-architecture/assets/images/react-logo@3x.png +0 -0
  19. package/templates/react-native/expo-clean-architecture/assets/images/splash-icon.png +0 -0
  20. package/templates/react-native/expo-clean-architecture/babel.config.js +11 -0
  21. package/templates/react-native/expo-clean-architecture/docs/00-introduction.md +3 -0
  22. package/templates/react-native/expo-clean-architecture/docs/01-architecture.md +107 -0
  23. package/templates/react-native/expo-clean-architecture/package.json +78 -0
  24. package/templates/react-native/expo-clean-architecture/scripts/clean-src.sh +48 -0
  25. package/templates/react-native/expo-clean-architecture/scripts/generate-feature.sh +40 -0
  26. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/_layout.tsx +42 -0
  27. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/favorites.tsx +72 -0
  28. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/home.tsx +122 -0
  29. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/_layout.tsx +5 -0
  30. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/index.tsx +29 -0
  31. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/profile.tsx +22 -0
  32. package/templates/react-native/expo-clean-architecture/src/app/(protected)/_layout.tsx +20 -0
  33. package/templates/react-native/expo-clean-architecture/src/app/(protected)/details.tsx +124 -0
  34. package/templates/react-native/expo-clean-architecture/src/app/(public)/_layout.tsx +18 -0
  35. package/templates/react-native/expo-clean-architecture/src/app/(public)/login.tsx +31 -0
  36. package/templates/react-native/expo-clean-architecture/src/app/_layout.tsx +33 -0
  37. package/templates/react-native/expo-clean-architecture/src/app/index.tsx +8 -0
  38. package/templates/react-native/expo-clean-architecture/src/core/constants/api-constants.ts +10 -0
  39. package/templates/react-native/expo-clean-architecture/src/core/constants/image-constants.ts +3 -0
  40. package/templates/react-native/expo-clean-architecture/src/core/constants/query-keys.ts +6 -0
  41. package/templates/react-native/expo-clean-architecture/src/core/constants/storage-keys.ts +3 -0
  42. package/templates/react-native/expo-clean-architecture/src/core/design/@types/color-scheme-state.ts +35 -0
  43. package/templates/react-native/expo-clean-architecture/src/core/design/@types/color-scheme.ts +12 -0
  44. package/templates/react-native/expo-clean-architecture/src/core/design/components/app-icon.tsx +16 -0
  45. package/templates/react-native/expo-clean-architecture/src/core/design/components/app-separator.tsx +26 -0
  46. package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-color-scheme.ts +52 -0
  47. package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-fonts.ts +12 -0
  48. package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-styles.ts +28 -0
  49. package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-colors.ts +21 -0
  50. package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-fonts.ts +16 -0
  51. package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-sizes.ts +14 -0
  52. package/templates/react-native/expo-clean-architecture/src/core/di/injection-container.ts +53 -0
  53. package/templates/react-native/expo-clean-architecture/src/core/errors/index.ts +1 -0
  54. package/templates/react-native/expo-clean-architecture/src/core/helpers/@types.ts +23 -0
  55. package/templates/react-native/expo-clean-architecture/src/core/helpers/rest-client.ts +144 -0
  56. package/templates/react-native/expo-clean-architecture/src/core/helpers/result.ts +37 -0
  57. package/templates/react-native/expo-clean-architecture/src/core/helpers/usecase.ts +5 -0
  58. package/templates/react-native/expo-clean-architecture/src/core/hooks/use-network.ts +18 -0
  59. package/templates/react-native/expo-clean-architecture/src/core/i18n/@types/i18next.d.ts +11 -0
  60. package/templates/react-native/expo-clean-architecture/src/core/i18n/index.ts +19 -0
  61. package/templates/react-native/expo-clean-architecture/src/core/i18n/translations/fr.json +12 -0
  62. package/templates/react-native/expo-clean-architecture/src/core/services/local-storage-service-impl.ts +29 -0
  63. package/templates/react-native/expo-clean-architecture/src/core/services/local-storage-service.ts +26 -0
  64. package/templates/react-native/expo-clean-architecture/src/core/services/monitoring-service-impl.ts +15 -0
  65. package/templates/react-native/expo-clean-architecture/src/core/services/monitoring-service.ts +13 -0
  66. package/templates/react-native/expo-clean-architecture/src/core/services/network-service-impl.ts +40 -0
  67. package/templates/react-native/expo-clean-architecture/src/core/services/network-service.ts +16 -0
  68. package/templates/react-native/expo-clean-architecture/src/features/auth/@types/session-state.ts +38 -0
  69. package/templates/react-native/expo-clean-architecture/src/features/auth/@types/session-status-enum.ts +16 -0
  70. package/templates/react-native/expo-clean-architecture/src/features/auth/presentation/hooks/use-session.ts +18 -0
  71. package/templates/react-native/expo-clean-architecture/src/features/favorites/data/datasources/favorites-datasource-impl.ts +25 -0
  72. package/templates/react-native/expo-clean-architecture/src/features/favorites/data/datasources/favorites-datasource.ts +5 -0
  73. package/templates/react-native/expo-clean-architecture/src/features/favorites/data/repositories/favorites-repository-impl.ts +46 -0
  74. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/repositories/favorites-repository.ts +8 -0
  75. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/add-to-favorites-usecase.ts +23 -0
  76. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/clear-favorites-usecase.ts +23 -0
  77. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/get-favorites-usecase.ts +24 -0
  78. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/remove-from-favorites-usecase.ts +23 -0
  79. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/components/favorites-card.tsx +77 -0
  80. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-add-favorite.ts +24 -0
  81. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-clear-favorites.ts +22 -0
  82. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-favorites.ts +22 -0
  83. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-remove-favorite.ts +24 -0
  84. package/templates/react-native/expo-clean-architecture/src/features/movies/data/datasources/movies-datasource-impl.ts +50 -0
  85. package/templates/react-native/expo-clean-architecture/src/features/movies/data/datasources/movies-datasource.ts +8 -0
  86. package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/movie-details-model.ts +67 -0
  87. package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/movie-model.ts +30 -0
  88. package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/tmdb-response-model.ts +6 -0
  89. package/templates/react-native/expo-clean-architecture/src/features/movies/data/repositories/movies-repository-impl.ts +34 -0
  90. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/entities/movie-details-entity.ts +28 -0
  91. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/entities/movie-entity.ts +6 -0
  92. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/repositories/movies-repository.ts +25 -0
  93. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/get-movie-details-usecase.ts +26 -0
  94. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/get-random-movies-usecase.ts +24 -0
  95. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/search-movies-usecase.ts +23 -0
  96. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/components/movie-tile.tsx +69 -0
  97. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-movie-details.ts +22 -0
  98. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-random-movies.ts +22 -0
  99. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-search-movies.ts +22 -0
  100. package/templates/react-native/expo-clean-architecture/tests/core/services/local-storage-service-impl.test.ts +108 -0
  101. package/templates/react-native/expo-clean-architecture/tests/core/services/monitoring-service.test.ts +74 -0
  102. package/templates/react-native/expo-clean-architecture/tests/core/services/network-service.test.ts +117 -0
  103. package/templates/react-native/expo-clean-architecture/tests/features/auth/presentation/hooks/use-session.test.ts +69 -0
  104. package/templates/react-native/expo-clean-architecture/tests/features/favorites/data/datasources/favorites-datasource.test.ts +69 -0
  105. package/templates/react-native/expo-clean-architecture/tests/features/favorites/data/repositories/favorites-repository-impl.test.ts +124 -0
  106. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/add-to-favorites-usecase.test.ts +54 -0
  107. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/clear-favorites-usecase.test.ts +44 -0
  108. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/get-favorites-usecase.test.ts +74 -0
  109. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/remove-from-favorites-usecase.test.ts +52 -0
  110. package/templates/react-native/expo-clean-architecture/tests/setup.ts +9 -0
  111. package/templates/react-native/expo-clean-architecture/tsconfig.json +20 -0
@@ -0,0 +1,108 @@
1
+ import { LocalStorageServiceImpl } from "@/core/services/local-storage-service-impl";
2
+ import AsyncStorage from "@react-native-async-storage/async-storage";
3
+
4
+ // Mock AsyncStorage
5
+ jest.mock("@react-native-async-storage/async-storage", () => ({
6
+ getItem: jest.fn(),
7
+ setItem: jest.fn(),
8
+ removeItem: jest.fn(),
9
+ clear: jest.fn(),
10
+ }));
11
+
12
+ describe("LocalStorageService", () => {
13
+ let service: LocalStorageServiceImpl;
14
+
15
+ beforeEach(() => {
16
+ // Clear all mocks before each test
17
+ jest.clearAllMocks();
18
+ service = new LocalStorageServiceImpl();
19
+ });
20
+
21
+ describe("getItem", () => {
22
+ it("should return stringified value when item exists", async () => {
23
+ const mockValue = { test: "data" };
24
+ (AsyncStorage.getItem as jest.Mock).mockResolvedValue(
25
+ JSON.stringify(mockValue)
26
+ );
27
+
28
+ const result = await service.getItem("test-key", "");
29
+ expect(result).toEqual(JSON.stringify(mockValue));
30
+ expect(AsyncStorage.getItem).toHaveBeenCalledWith("test-key");
31
+ });
32
+
33
+ it("should return default value when item doesn't exist", async () => {
34
+ (AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
35
+ const defaultValue = { default: "value" };
36
+
37
+ const result = await service.getItem(
38
+ "test-key",
39
+ JSON.stringify(defaultValue)
40
+ );
41
+
42
+ expect(result).toEqual(JSON.stringify(defaultValue));
43
+ expect(AsyncStorage.getItem).toHaveBeenCalledWith("test-key");
44
+ });
45
+ });
46
+
47
+ describe("setItem", () => {
48
+ it("should store stringified value", async () => {
49
+ const value = { test: "data" };
50
+ await service.setItem("test-key", JSON.stringify(value));
51
+
52
+ expect(AsyncStorage.setItem).toHaveBeenCalledWith(
53
+ "test-key",
54
+ JSON.stringify(value)
55
+ );
56
+ });
57
+ });
58
+
59
+ describe("removeItem", () => {
60
+ it("should remove item from storage", async () => {
61
+ await service.removeItem("test-key");
62
+ expect(AsyncStorage.removeItem).toHaveBeenCalledWith("test-key");
63
+ });
64
+ });
65
+
66
+ describe("clear", () => {
67
+ it("should clear all items from storage", async () => {
68
+ await service.clear();
69
+ expect(AsyncStorage.clear).toHaveBeenCalled();
70
+ });
71
+ });
72
+
73
+ describe("error handling", () => {
74
+ it("should throw error when getItem fails", async () => {
75
+ const error = new Error("Storage error");
76
+ (AsyncStorage.getItem as jest.Mock).mockRejectedValue(error);
77
+
78
+ await expect(service.getItem("test-key", "")).rejects.toThrow(
79
+ "Storage error"
80
+ );
81
+ });
82
+
83
+ it("should throw error when setItem fails", async () => {
84
+ const error = new Error("Storage error");
85
+ (AsyncStorage.setItem as jest.Mock).mockRejectedValue(error);
86
+
87
+ await expect(service.setItem("test-key", "value")).rejects.toThrow(
88
+ "Storage error"
89
+ );
90
+ });
91
+
92
+ it("should throw error when removeItem fails", async () => {
93
+ const error = new Error("Storage error");
94
+ (AsyncStorage.removeItem as jest.Mock).mockRejectedValue(error);
95
+
96
+ await expect(service.removeItem("test-key")).rejects.toThrow(
97
+ "Storage error"
98
+ );
99
+ });
100
+
101
+ it("should throw error when clear fails", async () => {
102
+ const error = new Error("Storage error");
103
+ (AsyncStorage.clear as jest.Mock).mockRejectedValue(error);
104
+
105
+ await expect(service.clear()).rejects.toThrow("Storage error");
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,74 @@
1
+ import { MonitoringServiceImpl } from "@/core/services/monitoring-service-impl";
2
+ import {
3
+ recordError,
4
+ getCrashlytics,
5
+ } from "@react-native-firebase/crashlytics";
6
+
7
+ // Mock @react-native-firebase/crashlytics
8
+ jest.mock("@react-native-firebase/crashlytics", () => ({
9
+ getCrashlytics: jest.fn(),
10
+ recordError: jest.fn(),
11
+ }));
12
+
13
+ describe("MonitoringServiceImpl", () => {
14
+ let monitoringService: MonitoringServiceImpl;
15
+ let mockCrashlytics: any;
16
+
17
+ beforeEach(() => {
18
+ // Reset all mocks before each test
19
+ jest.clearAllMocks();
20
+
21
+ // Setup mock crashlytics instance
22
+ mockCrashlytics = {};
23
+ (getCrashlytics as jest.Mock).mockReturnValue(mockCrashlytics);
24
+
25
+ monitoringService = new MonitoringServiceImpl();
26
+ });
27
+
28
+ describe("report", () => {
29
+ it("should report error to crashlytics", () => {
30
+ const testError = new Error("Test error");
31
+
32
+ monitoringService.report(testError);
33
+
34
+ expect(getCrashlytics).toHaveBeenCalledTimes(1);
35
+ expect(recordError).toHaveBeenCalledWith(mockCrashlytics, testError);
36
+ });
37
+
38
+ it("should handle different types of errors", () => {
39
+ // Test with TypeError
40
+ const typeError = new TypeError("Type error");
41
+ monitoringService.report(typeError);
42
+ expect(recordError).toHaveBeenCalledWith(mockCrashlytics, typeError);
43
+
44
+ // Test with ReferenceError
45
+ const referenceError = new ReferenceError("Reference error");
46
+ monitoringService.report(referenceError);
47
+ expect(recordError).toHaveBeenCalledWith(mockCrashlytics, referenceError);
48
+
49
+ // Test with custom error
50
+ const customError = new Error("Custom error");
51
+ customError.name = "CustomError";
52
+ monitoringService.report(customError);
53
+ expect(recordError).toHaveBeenCalledWith(mockCrashlytics, customError);
54
+ });
55
+
56
+ it("should handle error with additional properties", () => {
57
+ const error = new Error("Error with properties");
58
+ (error as any).code = "TEST_ERROR";
59
+ (error as any).metadata = { userId: "123" };
60
+
61
+ monitoringService.report(error);
62
+
63
+ expect(recordError).toHaveBeenCalledWith(mockCrashlytics, error);
64
+ // Verify that the error object with additional properties is passed as is
65
+ expect(recordError).toHaveBeenCalledWith(
66
+ mockCrashlytics,
67
+ expect.objectContaining({
68
+ code: "TEST_ERROR",
69
+ metadata: { userId: "123" },
70
+ })
71
+ );
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,117 @@
1
+ import { NetworkServiceImpl } from "@/core/services/network-service-impl";
2
+ import {
3
+ getNetworkStateAsync,
4
+ addNetworkStateListener,
5
+ NetworkState,
6
+ } from "expo-network";
7
+
8
+ // Mock expo-network
9
+ jest.mock("expo-network", () => ({
10
+ getNetworkStateAsync: jest.fn(),
11
+ addNetworkStateListener: jest.fn(),
12
+ }));
13
+
14
+ describe("NetworkServiceImpl", () => {
15
+ let networkService: NetworkServiceImpl;
16
+ let mockNetworkStateListener: (state: NetworkState) => void;
17
+
18
+ beforeEach(() => {
19
+ // Reset all mocks before each test
20
+ jest.clearAllMocks();
21
+
22
+ // Setup the mock for addNetworkStateListener to capture the callback
23
+ (addNetworkStateListener as jest.Mock).mockImplementation((callback) => {
24
+ mockNetworkStateListener = callback;
25
+ });
26
+
27
+ networkService = new NetworkServiceImpl();
28
+ });
29
+
30
+ describe("isOnline", () => {
31
+ it("should return true when network is reachable", async () => {
32
+ (getNetworkStateAsync as jest.Mock).mockResolvedValue({
33
+ isInternetReachable: true,
34
+ });
35
+
36
+ const result = await networkService.isOnline();
37
+ expect(result).toBe(true);
38
+ expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ it("should return false when network is not reachable", async () => {
42
+ (getNetworkStateAsync as jest.Mock).mockResolvedValue({
43
+ isInternetReachable: false,
44
+ });
45
+
46
+ const result = await networkService.isOnline();
47
+ expect(result).toBe(false);
48
+ expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ it("should return false when isInternetReachable is null", async () => {
52
+ (getNetworkStateAsync as jest.Mock).mockResolvedValue({
53
+ isInternetReachable: null,
54
+ });
55
+
56
+ const result = await networkService.isOnline();
57
+ expect(result).toBe(false);
58
+ expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
59
+ });
60
+ });
61
+
62
+ describe("subscribe", () => {
63
+ it("should call callback with initial network state", async () => {
64
+ const mockCallback = jest.fn();
65
+ (getNetworkStateAsync as jest.Mock).mockResolvedValue({
66
+ isInternetReachable: true,
67
+ });
68
+
69
+ networkService.subscribe(mockCallback);
70
+
71
+ // Wait for the initial state check
72
+ await new Promise(process.nextTick);
73
+
74
+ expect(mockCallback).toHaveBeenCalledWith(true);
75
+ expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
76
+ });
77
+
78
+ it("should notify subscribers when network state changes", async () => {
79
+ const mockCallback1 = jest.fn();
80
+ const mockCallback2 = jest.fn();
81
+
82
+ networkService.subscribe(mockCallback1);
83
+ networkService.subscribe(mockCallback2);
84
+
85
+ // Simulate network state change
86
+ mockNetworkStateListener({ isInternetReachable: true });
87
+ expect(mockCallback1).toHaveBeenCalledWith(true);
88
+ expect(mockCallback2).toHaveBeenCalledWith(true);
89
+
90
+ // Simulate another network state change
91
+ mockNetworkStateListener({ isInternetReachable: false });
92
+ expect(mockCallback1).toHaveBeenCalledWith(false);
93
+ expect(mockCallback2).toHaveBeenCalledWith(false);
94
+
95
+ // Simulate network state change with undefined isInternetReachable
96
+ mockNetworkStateListener({ isInternetReachable: undefined });
97
+ expect(mockCallback1).toHaveBeenCalledWith(false);
98
+ expect(mockCallback2).toHaveBeenCalledWith(false);
99
+ });
100
+
101
+ it("should allow unsubscribing from network state changes", async () => {
102
+ const mockCallback = jest.fn();
103
+ const unsubscribe = networkService.subscribe(mockCallback);
104
+
105
+ // Simulate network state change
106
+ mockNetworkStateListener({ isInternetReachable: true });
107
+ expect(mockCallback).toHaveBeenCalledWith(true);
108
+
109
+ // Unsubscribe
110
+ unsubscribe();
111
+
112
+ // Simulate another network state change
113
+ mockNetworkStateListener({ isInternetReachable: false });
114
+ expect(mockCallback).not.toHaveBeenCalledWith(false);
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,69 @@
1
+ import { useSession } from "@/features/auth/presentation/hooks/use-session";
2
+ import { SessionStatus } from "@/features/auth/@types/session-status-enum";
3
+
4
+ describe("useSession", () => {
5
+ beforeEach(() => {
6
+ // Reset the store to its initial state before each test
7
+ useSession.setState({
8
+ status: SessionStatus.UNAUTHENTICATED,
9
+ isInitialized: false,
10
+ });
11
+ });
12
+
13
+ describe("initial state", () => {
14
+ it("should have correct initial state", () => {
15
+ const state = useSession.getState();
16
+
17
+ expect(state.status).toBe(SessionStatus.UNAUTHENTICATED);
18
+ expect(state.isInitialized).toBe(false);
19
+ expect(state.isAuthenticated()).toBe(false);
20
+ });
21
+ });
22
+
23
+ describe("login", () => {
24
+ it("should update status to AUTHENTICATED when login is called", async () => {
25
+ await useSession.getState().login();
26
+
27
+ const state = useSession.getState();
28
+ expect(state.status).toBe(SessionStatus.AUTHENTICATED);
29
+ expect(state.isAuthenticated()).toBe(true);
30
+ });
31
+ });
32
+
33
+ describe("logout", () => {
34
+ it("should update status to UNAUTHENTICATED when logout is called", async () => {
35
+ // First login to set authenticated state
36
+ await useSession.getState().login();
37
+ expect(useSession.getState().status).toBe(SessionStatus.AUTHENTICATED);
38
+
39
+ // Then logout
40
+ await useSession.getState().logout();
41
+
42
+ const state = useSession.getState();
43
+ expect(state.status).toBe(SessionStatus.UNAUTHENTICATED);
44
+ expect(state.isAuthenticated()).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe("silentLogin", () => {
49
+ it("should update status to AUTHENTICATED when silentLogin is called", async () => {
50
+ await useSession.getState().silentLogin();
51
+
52
+ const state = useSession.getState();
53
+ expect(state.status).toBe(SessionStatus.AUTHENTICATED);
54
+ expect(state.isAuthenticated()).toBe(true);
55
+ });
56
+ });
57
+
58
+ describe("isAuthenticated", () => {
59
+ it("should return true when status is AUTHENTICATED", () => {
60
+ useSession.setState({ status: SessionStatus.AUTHENTICATED });
61
+ expect(useSession.getState().isAuthenticated()).toBe(true);
62
+ });
63
+
64
+ it("should return false when status is UNAUTHENTICATED", () => {
65
+ useSession.setState({ status: SessionStatus.UNAUTHENTICATED });
66
+ expect(useSession.getState().isAuthenticated()).toBe(false);
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,69 @@
1
+ import { LocalStorageService } from "@/core/services/local-storage-service";
2
+ import { STORAGE_KEYS } from "@/core/constants/storage-keys";
3
+ import { FavoritesDatasourceImpl } from "@/features/favorites/data/datasources/favorites-datasource-impl";
4
+
5
+ describe("FavoritesDatasourceImpl", () => {
6
+ let datasource: FavoritesDatasourceImpl;
7
+ let mockLocalStorageService: jest.Mocked<LocalStorageService>;
8
+
9
+ beforeEach(() => {
10
+ mockLocalStorageService = {
11
+ getItem: jest.fn(),
12
+ setItem: jest.fn(),
13
+ removeItem: jest.fn(),
14
+ clear: jest.fn(),
15
+ } as unknown as jest.Mocked<LocalStorageService>;
16
+
17
+ datasource = new FavoritesDatasourceImpl(mockLocalStorageService);
18
+ });
19
+
20
+ describe("getFavorites", () => {
21
+ it("should return favorites from localStorage with default empty array", async () => {
22
+ const mockFavorites = '["movie1", "movie2"]';
23
+ mockLocalStorageService.getItem.mockResolvedValue(mockFavorites);
24
+
25
+ const result = await datasource.getFavorites();
26
+
27
+ expect(mockLocalStorageService.getItem).toHaveBeenCalledWith(
28
+ STORAGE_KEYS.FAVORITES,
29
+ "[]"
30
+ );
31
+ expect(result).toBe(mockFavorites);
32
+ });
33
+
34
+ it("should return default empty array when no favorites exist", async () => {
35
+ mockLocalStorageService.getItem.mockResolvedValue("[]");
36
+
37
+ const result = await datasource.getFavorites();
38
+
39
+ expect(mockLocalStorageService.getItem).toHaveBeenCalledWith(
40
+ STORAGE_KEYS.FAVORITES,
41
+ "[]"
42
+ );
43
+ expect(result).toBe("[]");
44
+ });
45
+ });
46
+
47
+ describe("setFavorites", () => {
48
+ it("should save favorites to localStorage", async () => {
49
+ const mockFavorites = '["movie1", "movie2"]';
50
+
51
+ await datasource.setFavorites(mockFavorites);
52
+
53
+ expect(mockLocalStorageService.setItem).toHaveBeenCalledWith(
54
+ STORAGE_KEYS.FAVORITES,
55
+ mockFavorites
56
+ );
57
+ });
58
+ });
59
+
60
+ describe("clearFavorites", () => {
61
+ it("should remove favorites from localStorage", async () => {
62
+ await datasource.clearFavorites();
63
+
64
+ expect(mockLocalStorageService.removeItem).toHaveBeenCalledWith(
65
+ STORAGE_KEYS.FAVORITES
66
+ );
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,124 @@
1
+ import { FavoritesRepositoryImpl } from "@/features/favorites/data/repositories/favorites-repository-impl";
2
+ import { FavoritesDatasource } from "@/features/favorites/data/datasources/favorites-datasource";
3
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
4
+
5
+ describe("FavoritesRepositoryImpl", () => {
6
+ let repository: FavoritesRepositoryImpl;
7
+ let mockFavoritesDatasource: jest.Mocked<FavoritesDatasource>;
8
+
9
+ const mockMovie: MovieEntity = {
10
+ id: "1",
11
+ title: "Test Movie",
12
+ description: "Test Description",
13
+ image: "/test.jpg",
14
+ };
15
+
16
+ const mockFavorites = [mockMovie];
17
+
18
+ beforeEach(() => {
19
+ mockFavoritesDatasource = {
20
+ getFavorites: jest.fn(),
21
+ setFavorites: jest.fn(),
22
+ clearFavorites: jest.fn(),
23
+ } as jest.Mocked<FavoritesDatasource>;
24
+
25
+ repository = new FavoritesRepositoryImpl(mockFavoritesDatasource);
26
+ });
27
+
28
+ describe("addFavorite", () => {
29
+ it("should add a movie to favorites", async () => {
30
+ mockFavoritesDatasource.getFavorites.mockResolvedValue(
31
+ JSON.stringify([])
32
+ );
33
+ mockFavoritesDatasource.setFavorites.mockResolvedValue();
34
+
35
+ await repository.addFavorite(mockMovie);
36
+
37
+ expect(mockFavoritesDatasource.getFavorites).toHaveBeenCalled();
38
+ expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
39
+ JSON.stringify([mockMovie])
40
+ );
41
+ });
42
+
43
+ it("should append movie to existing favorites", async () => {
44
+ const existingMovie: MovieEntity = {
45
+ ...mockMovie,
46
+ id: "2",
47
+ title: "Existing Movie",
48
+ };
49
+
50
+ mockFavoritesDatasource.getFavorites.mockResolvedValue(
51
+ JSON.stringify([existingMovie])
52
+ );
53
+ mockFavoritesDatasource.setFavorites.mockResolvedValue();
54
+
55
+ await repository.addFavorite(mockMovie);
56
+
57
+ expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
58
+ JSON.stringify([existingMovie, mockMovie])
59
+ );
60
+ });
61
+ });
62
+
63
+ describe("removeFavorite", () => {
64
+ it("should remove a movie from favorites", async () => {
65
+ mockFavoritesDatasource.getFavorites.mockResolvedValue(
66
+ JSON.stringify(mockFavorites)
67
+ );
68
+ mockFavoritesDatasource.setFavorites.mockResolvedValue();
69
+
70
+ await repository.removeFavorite(mockMovie.id);
71
+
72
+ expect(mockFavoritesDatasource.getFavorites).toHaveBeenCalled();
73
+ expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
74
+ JSON.stringify([])
75
+ );
76
+ });
77
+
78
+ it("should not modify favorites if movie is not found", async () => {
79
+ mockFavoritesDatasource.getFavorites.mockResolvedValue(
80
+ JSON.stringify(mockFavorites)
81
+ );
82
+ mockFavoritesDatasource.setFavorites.mockResolvedValue();
83
+
84
+ await repository.removeFavorite("non-existent-id");
85
+
86
+ expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
87
+ JSON.stringify(mockFavorites)
88
+ );
89
+ });
90
+ });
91
+
92
+ describe("getFavorites", () => {
93
+ it("should return all favorites", async () => {
94
+ mockFavoritesDatasource.getFavorites.mockResolvedValue(
95
+ JSON.stringify(mockFavorites)
96
+ );
97
+
98
+ const result = await repository.getFavorites();
99
+
100
+ expect(mockFavoritesDatasource.getFavorites).toHaveBeenCalled();
101
+ expect(result).toEqual(mockFavorites);
102
+ });
103
+
104
+ it("should return empty array when no favorites exist", async () => {
105
+ mockFavoritesDatasource.getFavorites.mockResolvedValue(
106
+ JSON.stringify([])
107
+ );
108
+
109
+ const result = await repository.getFavorites();
110
+
111
+ expect(result).toEqual([]);
112
+ });
113
+ });
114
+
115
+ describe("clearFavorites", () => {
116
+ it("should clear all favorites", async () => {
117
+ mockFavoritesDatasource.clearFavorites.mockResolvedValue();
118
+
119
+ await repository.clearFavorites();
120
+
121
+ expect(mockFavoritesDatasource.clearFavorites).toHaveBeenCalled();
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,54 @@
1
+ import { AddToFavoritesUsecase } from "@/features/favorites/domain/usecases/add-to-favorites-usecase";
2
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
3
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
4
+
5
+ describe("AddToFavoritesUsecase", () => {
6
+ let usecase: AddToFavoritesUsecase;
7
+ let mockFavoritesRepository: jest.Mocked<FavoritesRepository>;
8
+
9
+ const mockMovie: MovieEntity = {
10
+ id: "123",
11
+ title: "Test Movie",
12
+ description: "Test Description",
13
+ image: "test-image.jpg",
14
+ };
15
+
16
+ beforeEach(() => {
17
+ mockFavoritesRepository = {
18
+ getFavorites: jest.fn(),
19
+ addFavorite: jest.fn(),
20
+ removeFavorite: jest.fn(),
21
+ clearFavorites: jest.fn(),
22
+ };
23
+
24
+ usecase = new AddToFavoritesUsecase(mockFavoritesRepository);
25
+ });
26
+
27
+ it("should successfully add a movie to favorites", async () => {
28
+ // Arrange
29
+ mockFavoritesRepository.addFavorite.mockResolvedValue();
30
+
31
+ // Act
32
+ const result = await usecase.execute(mockMovie);
33
+
34
+ // Assert
35
+ expect(result.isSuccess).toBe(true);
36
+ expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledWith(mockMovie);
37
+ expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it("should return failure when repository throws an error", async () => {
41
+ // Arrange
42
+ const error = new Error("Failed to add favorite");
43
+ mockFavoritesRepository.addFavorite.mockRejectedValue(error);
44
+
45
+ // Act
46
+ const result = await usecase.execute(mockMovie);
47
+
48
+ // Assert
49
+ expect(result.isFailure).toBe(true);
50
+ expect(result.error).toBe(error);
51
+ expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledWith(mockMovie);
52
+ expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledTimes(1);
53
+ });
54
+ });
@@ -0,0 +1,44 @@
1
+ import { ClearFavoritesUsecase } from "@/features/favorites/domain/usecases/clear-favorites-usecase";
2
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
3
+
4
+ describe("ClearFavoritesUsecase", () => {
5
+ let usecase: ClearFavoritesUsecase;
6
+ let mockFavoritesRepository: jest.Mocked<FavoritesRepository>;
7
+
8
+ beforeEach(() => {
9
+ mockFavoritesRepository = {
10
+ getFavorites: jest.fn(),
11
+ addFavorite: jest.fn(),
12
+ removeFavorite: jest.fn(),
13
+ clearFavorites: jest.fn(),
14
+ };
15
+
16
+ usecase = new ClearFavoritesUsecase(mockFavoritesRepository);
17
+ });
18
+
19
+ it("should successfully clear all favorites", async () => {
20
+ // Arrange
21
+ mockFavoritesRepository.clearFavorites.mockResolvedValue();
22
+
23
+ // Act
24
+ const result = await usecase.execute();
25
+
26
+ // Assert
27
+ expect(result.isSuccess).toBe(true);
28
+ expect(mockFavoritesRepository.clearFavorites).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ it("should return failure when repository throws an error", async () => {
32
+ // Arrange
33
+ const error = new Error("Failed to clear favorites");
34
+ mockFavoritesRepository.clearFavorites.mockRejectedValue(error);
35
+
36
+ // Act
37
+ const result = await usecase.execute();
38
+
39
+ // Assert
40
+ expect(result.isFailure).toBe(true);
41
+ expect(result.error).toBe(error);
42
+ expect(mockFavoritesRepository.clearFavorites).toHaveBeenCalledTimes(1);
43
+ });
44
+ });