xfeed 0.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.
- package/LICENSE +23 -0
- package/README.md +5 -0
- package/package.json +43 -0
- package/src/api/actions.ts +16 -0
- package/src/api/client.test.ts +3370 -0
- package/src/api/client.ts +4319 -0
- package/src/api/query-ids.json +11 -0
- package/src/api/query-ids.test.ts +118 -0
- package/src/api/query-ids.ts +59 -0
- package/src/api/runtime-query-ids.test.ts +926 -0
- package/src/api/runtime-query-ids.ts +389 -0
- package/src/api/types.ts +581 -0
- package/src/app.tsx +664 -0
- package/src/auth/browser-detect.ts +150 -0
- package/src/auth/browser-picker.ts +118 -0
- package/src/auth/check.test.preload.ts +94 -0
- package/src/auth/check.test.ts +388 -0
- package/src/auth/check.ts +220 -0
- package/src/auth/cookies.test.ts +529 -0
- package/src/auth/cookies.ts +299 -0
- package/src/auth/manual-entry.ts +88 -0
- package/src/auth/session.ts +30 -0
- package/src/components/ErrorBanner.tsx +172 -0
- package/src/components/Footer.tsx +90 -0
- package/src/components/Header.tsx +57 -0
- package/src/components/NotificationItem.test.ts +252 -0
- package/src/components/NotificationItem.tsx +80 -0
- package/src/components/NotificationList.test.ts +328 -0
- package/src/components/NotificationList.tsx +157 -0
- package/src/components/PostCard.tsx +186 -0
- package/src/components/PostList.tsx +232 -0
- package/src/components/QuotedPostCard.tsx +55 -0
- package/src/components/ReplyPreviewCard.tsx +80 -0
- package/src/components/ThreadView.prototype.tsx +533 -0
- package/src/components/Toast.tsx +28 -0
- package/src/config/loader.ts +69 -0
- package/src/config/types.ts +27 -0
- package/src/contexts/ModalContext.tsx +227 -0
- package/src/experiments/TimelineScreenExperimental.tsx +202 -0
- package/src/experiments/index.tsx +43 -0
- package/src/experiments/query-client.ts +132 -0
- package/src/experiments/use-bookmark-mutation.ts +342 -0
- package/src/experiments/use-bookmarks-query.ts +166 -0
- package/src/experiments/use-notifications-query.ts +368 -0
- package/src/experiments/use-post-detail-query.ts +187 -0
- package/src/experiments/use-profile-query.ts +162 -0
- package/src/experiments/use-timeline-query.ts +201 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/useActions.ts +354 -0
- package/src/hooks/useBookmarkFolders.ts +70 -0
- package/src/hooks/useBookmarks.ts +111 -0
- package/src/hooks/useCountdown.ts +75 -0
- package/src/hooks/useListNavigation.test.ts +273 -0
- package/src/hooks/useListNavigation.ts +118 -0
- package/src/hooks/useNavigation.test.ts +340 -0
- package/src/hooks/useNavigation.ts +103 -0
- package/src/hooks/useNotifications.test.ts +377 -0
- package/src/hooks/useNotifications.ts +117 -0
- package/src/hooks/usePaginatedData.ts +217 -0
- package/src/hooks/usePostDetail.ts +137 -0
- package/src/hooks/useThread.prototype.ts +314 -0
- package/src/hooks/useTimeline.ts +136 -0
- package/src/hooks/useUserProfile.ts +142 -0
- package/src/index.tsx +304 -0
- package/src/lib/colors.ts +41 -0
- package/src/lib/format.ts +69 -0
- package/src/lib/media.ts +464 -0
- package/src/lib/result.ts +6 -0
- package/src/lib/text.tsx +76 -0
- package/src/modals/BookmarkFolderSelector.tsx +260 -0
- package/src/modals/ExitConfirmationModal.tsx +131 -0
- package/src/modals/FolderPicker.tsx +281 -0
- package/src/modals/README.md +171 -0
- package/src/modals/SessionExpiredModal.tsx +47 -0
- package/src/modals/index.ts +4 -0
- package/src/screens/.gitkeep +0 -0
- package/src/screens/BookmarksScreen.tsx +168 -0
- package/src/screens/NotificationsScreen.tsx +172 -0
- package/src/screens/PostDetailScreen.tsx +976 -0
- package/src/screens/ProfileScreen.tsx +528 -0
- package/src/screens/SplashScreen.tsx +72 -0
- package/src/screens/ThreadScreen.tsx +81 -0
- package/src/screens/TimelineScreen.tsx +188 -0
- package/vendor/sweet-cookie/LICENSE +22 -0
- package/vendor/sweet-cookie/README.md +29 -0
- package/vendor/sweet-cookie/dist/index.d.ts +3 -0
- package/vendor/sweet-cookie/dist/index.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/index.js +2 -0
- package/vendor/sweet-cookie/dist/index.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chrome.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chrome.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chrome.js +27 -0
- package/vendor/sweet-cookie/dist/providers/chrome.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts +11 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.js +100 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts +25 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js +104 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.js +293 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js +26 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.js +51 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.js +118 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.js +38 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts +5 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.js +33 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts +24 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.js +30 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.d.ts +11 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.js +43 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.js +41 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.js +53 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edge.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/edge.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edge.js +27 -0
- package/vendor/sweet-cookie/dist/providers/edge.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.js +53 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.js +60 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.js +38 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.d.ts +6 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.js +257 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/inline.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/inline.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/inline.js +71 -0
- package/vendor/sweet-cookie/dist/providers/inline.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.d.ts +6 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.js +173 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.js.map +1 -0
- package/vendor/sweet-cookie/dist/public.d.ts +26 -0
- package/vendor/sweet-cookie/dist/public.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/public.js +197 -0
- package/vendor/sweet-cookie/dist/public.js.map +1 -0
- package/vendor/sweet-cookie/dist/types.d.ts +127 -0
- package/vendor/sweet-cookie/dist/types.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/types.js +2 -0
- package/vendor/sweet-cookie/dist/types.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/base64.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/base64.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/base64.js +18 -0
- package/vendor/sweet-cookie/dist/util/base64.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/exec.d.ts +8 -0
- package/vendor/sweet-cookie/dist/util/exec.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/exec.js +110 -0
- package/vendor/sweet-cookie/dist/util/exec.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/expire.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/expire.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/expire.js +32 -0
- package/vendor/sweet-cookie/dist/util/expire.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/fs.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/fs.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/fs.js +13 -0
- package/vendor/sweet-cookie/dist/util/fs.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.js +7 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.d.ts +5 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.js +58 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/origins.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/origins.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/origins.js +27 -0
- package/vendor/sweet-cookie/dist/util/origins.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/runtime.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/runtime.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/runtime.js +8 -0
- package/vendor/sweet-cookie/dist/util/runtime.js.map +1 -0
- package/vendor/sweet-cookie/package.json +40 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// @ts-nocheck - Test file with module mocking
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for check.ts
|
|
4
|
+
* Tests authentication validation and error handling
|
|
5
|
+
*
|
|
6
|
+
* NOTE: This test uses a preload file for module mocking to avoid
|
|
7
|
+
* polluting other test files. Run with:
|
|
8
|
+
* bun test --preload ./src/auth/check.test.preload.ts src/auth/check.test.ts
|
|
9
|
+
*
|
|
10
|
+
* Or run all tests in isolation mode (configured in bunfig.toml)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import type { XCookies } from "./cookies";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
lastClientOptions,
|
|
19
|
+
resetMocks,
|
|
20
|
+
setLastClientOptions,
|
|
21
|
+
setMockGetCurrentUser,
|
|
22
|
+
setMockResolveCredentials,
|
|
23
|
+
} from "./check.test.preload";
|
|
24
|
+
|
|
25
|
+
// Import the module under test (mocks are set up by preload)
|
|
26
|
+
const { checkAuth, formatWarnings, getAuthErrorMessage } =
|
|
27
|
+
await import("./check");
|
|
28
|
+
|
|
29
|
+
// Helper to create valid cookies
|
|
30
|
+
function createValidCookies(overrides: Partial<XCookies> = {}): XCookies {
|
|
31
|
+
return {
|
|
32
|
+
authToken: "test-auth-token",
|
|
33
|
+
ct0: "test-ct0",
|
|
34
|
+
cookieHeader: "auth_token=test-auth-token; ct0=test-ct0",
|
|
35
|
+
source: "test",
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("check", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
resetMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
setLastClientOptions(null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("checkAuth", () => {
|
|
50
|
+
describe("missing credentials", () => {
|
|
51
|
+
it("returns missing_credentials error when authToken is missing", async () => {
|
|
52
|
+
setMockResolveCredentials(() =>
|
|
53
|
+
Promise.resolve({
|
|
54
|
+
cookies: createValidCookies({ authToken: null }),
|
|
55
|
+
warnings: ["No auth_token found"],
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const result = await checkAuth();
|
|
60
|
+
|
|
61
|
+
expect(result.ok).toBe(false);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
expect(result.errorType).toBe("missing_credentials");
|
|
64
|
+
expect(result.error).toContain("Not authenticated");
|
|
65
|
+
expect(result.warnings).toContain("No auth_token found");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns missing_credentials error when ct0 is missing", async () => {
|
|
70
|
+
setMockResolveCredentials(() =>
|
|
71
|
+
Promise.resolve({
|
|
72
|
+
cookies: createValidCookies({ ct0: null }),
|
|
73
|
+
warnings: ["No ct0 found"],
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const result = await checkAuth();
|
|
78
|
+
|
|
79
|
+
expect(result.ok).toBe(false);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
expect(result.errorType).toBe("missing_credentials");
|
|
82
|
+
expect(result.error).toContain("Not authenticated");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns missing_credentials error when both cookies are missing", async () => {
|
|
87
|
+
setMockResolveCredentials(() =>
|
|
88
|
+
Promise.resolve({
|
|
89
|
+
cookies: {
|
|
90
|
+
authToken: null,
|
|
91
|
+
ct0: null,
|
|
92
|
+
cookieHeader: null,
|
|
93
|
+
source: null,
|
|
94
|
+
},
|
|
95
|
+
warnings: ["No cookies found"],
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const result = await checkAuth();
|
|
100
|
+
|
|
101
|
+
expect(result.ok).toBe(false);
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
expect(result.errorType).toBe("missing_credentials");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("expired session", () => {
|
|
109
|
+
it("returns expired_session error on 401 response", async () => {
|
|
110
|
+
setMockGetCurrentUser(() =>
|
|
111
|
+
Promise.resolve({
|
|
112
|
+
success: false,
|
|
113
|
+
error: "HTTP 401: Unauthorized",
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const result = await checkAuth();
|
|
118
|
+
|
|
119
|
+
expect(result.ok).toBe(false);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
expect(result.errorType).toBe("expired_session");
|
|
122
|
+
expect(result.error).toContain("Session expired");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns expired_session error on 403 response", async () => {
|
|
127
|
+
setMockGetCurrentUser(() =>
|
|
128
|
+
Promise.resolve({
|
|
129
|
+
success: false,
|
|
130
|
+
error: "HTTP 403: Forbidden",
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = await checkAuth();
|
|
135
|
+
|
|
136
|
+
expect(result.ok).toBe(false);
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
expect(result.errorType).toBe("expired_session");
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns expired_session error on unauthorized message", async () => {
|
|
143
|
+
setMockGetCurrentUser(() =>
|
|
144
|
+
Promise.resolve({
|
|
145
|
+
success: false,
|
|
146
|
+
error: "Bad authentication data",
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const result = await checkAuth();
|
|
151
|
+
|
|
152
|
+
expect(result.ok).toBe(false);
|
|
153
|
+
if (!result.ok) {
|
|
154
|
+
expect(result.errorType).toBe("expired_session");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("network errors", () => {
|
|
160
|
+
it("returns network_error on connection refused", async () => {
|
|
161
|
+
setMockGetCurrentUser(() =>
|
|
162
|
+
Promise.resolve({
|
|
163
|
+
success: false,
|
|
164
|
+
error: "ECONNREFUSED",
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const result = await checkAuth();
|
|
169
|
+
|
|
170
|
+
expect(result.ok).toBe(false);
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
expect(result.errorType).toBe("network_error");
|
|
173
|
+
expect(result.error).toContain("Network error");
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns network_error on timeout", async () => {
|
|
178
|
+
setMockGetCurrentUser(() =>
|
|
179
|
+
Promise.resolve({
|
|
180
|
+
success: false,
|
|
181
|
+
error: "ETIMEDOUT",
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const result = await checkAuth();
|
|
186
|
+
|
|
187
|
+
expect(result.ok).toBe(false);
|
|
188
|
+
if (!result.ok) {
|
|
189
|
+
expect(result.errorType).toBe("network_error");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns network_error on fetch failed", async () => {
|
|
194
|
+
setMockGetCurrentUser(() =>
|
|
195
|
+
Promise.resolve({
|
|
196
|
+
success: false,
|
|
197
|
+
error: "fetch failed",
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const result = await checkAuth();
|
|
202
|
+
|
|
203
|
+
expect(result.ok).toBe(false);
|
|
204
|
+
if (!result.ok) {
|
|
205
|
+
expect(result.errorType).toBe("network_error");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("successful authentication", () => {
|
|
211
|
+
it("returns client and user on success", async () => {
|
|
212
|
+
const result = await checkAuth();
|
|
213
|
+
|
|
214
|
+
expect(result.ok).toBe(true);
|
|
215
|
+
if (result.ok) {
|
|
216
|
+
expect(result.user.username).toBe("testuser");
|
|
217
|
+
expect(result.user.id).toBe("123");
|
|
218
|
+
expect(result.user.name).toBe("Test User");
|
|
219
|
+
expect(result.client).toBeDefined();
|
|
220
|
+
expect(result.cookies.authToken).toBe("test-auth-token");
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("passes options to resolveCredentials", async () => {
|
|
225
|
+
let capturedOptions: unknown = null;
|
|
226
|
+
setMockResolveCredentials((options) => {
|
|
227
|
+
capturedOptions = options;
|
|
228
|
+
return Promise.resolve({
|
|
229
|
+
cookies: createValidCookies(),
|
|
230
|
+
warnings: [],
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await checkAuth({
|
|
235
|
+
authToken: "cli-token",
|
|
236
|
+
ct0: "cli-ct0",
|
|
237
|
+
cookieSource: "chrome",
|
|
238
|
+
chromeProfile: "Profile 1",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(capturedOptions).toEqual({
|
|
242
|
+
authToken: "cli-token",
|
|
243
|
+
ct0: "cli-ct0",
|
|
244
|
+
cookieSource: "chrome",
|
|
245
|
+
chromeProfile: "Profile 1",
|
|
246
|
+
firefoxProfile: undefined,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("passes timeoutMs to XClient", async () => {
|
|
251
|
+
await checkAuth({ timeoutMs: 5000 });
|
|
252
|
+
|
|
253
|
+
expect(lastClientOptions?.timeoutMs).toBe(5000);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("includes warnings on success", async () => {
|
|
257
|
+
setMockResolveCredentials(() =>
|
|
258
|
+
Promise.resolve({
|
|
259
|
+
cookies: createValidCookies(),
|
|
260
|
+
warnings: ["Using fallback browser"],
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const result = await checkAuth();
|
|
265
|
+
|
|
266
|
+
expect(result.ok).toBe(true);
|
|
267
|
+
if (result.ok) {
|
|
268
|
+
expect(result.warnings).toContain("Using fallback browser");
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("skip validation", () => {
|
|
274
|
+
it("skips API call when skipValidation is true", async () => {
|
|
275
|
+
let getCurrentUserCalled = false;
|
|
276
|
+
setMockGetCurrentUser(() => {
|
|
277
|
+
getCurrentUserCalled = true;
|
|
278
|
+
return Promise.resolve({ success: true, user: undefined });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const result = await checkAuth({ skipValidation: true });
|
|
282
|
+
|
|
283
|
+
expect(result.ok).toBe(true);
|
|
284
|
+
expect(getCurrentUserCalled).toBe(false);
|
|
285
|
+
if (result.ok) {
|
|
286
|
+
expect(result.user.username).toBe("");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("still checks for missing credentials when skipValidation is true", async () => {
|
|
291
|
+
setMockResolveCredentials(() =>
|
|
292
|
+
Promise.resolve({
|
|
293
|
+
cookies: {
|
|
294
|
+
authToken: null,
|
|
295
|
+
ct0: null,
|
|
296
|
+
cookieHeader: null,
|
|
297
|
+
source: null,
|
|
298
|
+
},
|
|
299
|
+
warnings: [],
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const result = await checkAuth({ skipValidation: true });
|
|
304
|
+
|
|
305
|
+
expect(result.ok).toBe(false);
|
|
306
|
+
if (!result.ok) {
|
|
307
|
+
expect(result.errorType).toBe("missing_credentials");
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("unknown errors", () => {
|
|
313
|
+
it("returns unknown error for unrecognized API errors", async () => {
|
|
314
|
+
setMockGetCurrentUser(() =>
|
|
315
|
+
Promise.resolve({
|
|
316
|
+
success: false,
|
|
317
|
+
error: "Something unexpected happened",
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const result = await checkAuth();
|
|
322
|
+
|
|
323
|
+
expect(result.ok).toBe(false);
|
|
324
|
+
if (!result.ok) {
|
|
325
|
+
expect(result.errorType).toBe("unknown");
|
|
326
|
+
expect(result.error).toContain("Authentication failed");
|
|
327
|
+
expect(result.error).toContain("Something unexpected happened");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("returns unknown error when user is missing from success response", async () => {
|
|
332
|
+
setMockGetCurrentUser(() =>
|
|
333
|
+
Promise.resolve({
|
|
334
|
+
success: true,
|
|
335
|
+
user: undefined,
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const result = await checkAuth();
|
|
340
|
+
|
|
341
|
+
expect(result.ok).toBe(false);
|
|
342
|
+
if (!result.ok) {
|
|
343
|
+
expect(result.errorType).toBe("unknown");
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("getAuthErrorMessage", () => {
|
|
350
|
+
it("returns correct message for missing_credentials", () => {
|
|
351
|
+
const message = getAuthErrorMessage("missing_credentials");
|
|
352
|
+
expect(message).toContain("Not authenticated");
|
|
353
|
+
expect(message).toContain("log into x.com");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("returns correct message for expired_session", () => {
|
|
357
|
+
const message = getAuthErrorMessage("expired_session");
|
|
358
|
+
expect(message).toContain("Session expired");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("returns correct message for network_error", () => {
|
|
362
|
+
const message = getAuthErrorMessage("network_error");
|
|
363
|
+
expect(message).toContain("Network error");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("returns correct message for unknown", () => {
|
|
367
|
+
const message = getAuthErrorMessage("unknown");
|
|
368
|
+
expect(message).toContain("Authentication failed");
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("formatWarnings", () => {
|
|
373
|
+
it("returns empty string for no warnings", () => {
|
|
374
|
+
const result = formatWarnings([]);
|
|
375
|
+
expect(result).toBe("");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("formats single warning with bullet", () => {
|
|
379
|
+
const result = formatWarnings(["Something went wrong"]);
|
|
380
|
+
expect(result).toBe(" - Something went wrong");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("formats multiple warnings with bullets", () => {
|
|
384
|
+
const result = formatWarnings(["Warning 1", "Warning 2"]);
|
|
385
|
+
expect(result).toBe(" - Warning 1\n - Warning 2");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication validation and error handling.
|
|
3
|
+
* Validates credentials and provides clear error messages.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { UserData } from "@/api/types";
|
|
7
|
+
|
|
8
|
+
import { XClient } from "@/api/client";
|
|
9
|
+
|
|
10
|
+
import type { CookieSource, XCookies } from "./cookies";
|
|
11
|
+
|
|
12
|
+
import { resolveCredentials } from "./cookies";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auth error types for different failure modes
|
|
16
|
+
*/
|
|
17
|
+
export type AuthErrorType =
|
|
18
|
+
| "missing_credentials"
|
|
19
|
+
| "expired_session"
|
|
20
|
+
| "network_error"
|
|
21
|
+
| "unknown";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Auth check result - success with client or failure with error details
|
|
25
|
+
*/
|
|
26
|
+
export type AuthCheckResult =
|
|
27
|
+
| {
|
|
28
|
+
ok: true;
|
|
29
|
+
client: XClient;
|
|
30
|
+
user: UserData;
|
|
31
|
+
cookies: XCookies;
|
|
32
|
+
warnings: string[];
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
ok: false;
|
|
36
|
+
errorType: AuthErrorType;
|
|
37
|
+
error: string;
|
|
38
|
+
warnings: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options for checkAuth
|
|
43
|
+
*/
|
|
44
|
+
export interface CheckAuthOptions {
|
|
45
|
+
/** Auth token from CLI argument */
|
|
46
|
+
authToken?: string;
|
|
47
|
+
/** CSRF token from CLI argument */
|
|
48
|
+
ct0?: string;
|
|
49
|
+
/** Browser(s) to try for cookie extraction */
|
|
50
|
+
cookieSource?: CookieSource | CookieSource[];
|
|
51
|
+
/** Chrome profile name */
|
|
52
|
+
chromeProfile?: string;
|
|
53
|
+
/** Firefox profile name */
|
|
54
|
+
firefoxProfile?: string;
|
|
55
|
+
/** Skip API validation (just check if cookies exist) */
|
|
56
|
+
skipValidation?: boolean;
|
|
57
|
+
/** Timeout for API requests in milliseconds */
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* User-friendly error messages for different error types
|
|
63
|
+
*/
|
|
64
|
+
const ERROR_MESSAGES: Record<AuthErrorType, string> = {
|
|
65
|
+
missing_credentials:
|
|
66
|
+
"Not authenticated.\nPlease log into x.com in your browser (Safari/Chrome/Firefox).\nxfeed reads cookies automatically - no manual setup needed.",
|
|
67
|
+
expired_session:
|
|
68
|
+
"Session expired.\nPlease log into x.com in your browser and restart xfeed.",
|
|
69
|
+
network_error:
|
|
70
|
+
"Network error while validating authentication.\nPlease check your internet connection and try again.",
|
|
71
|
+
unknown: "Authentication failed.\nPlease try logging into x.com again.",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if an error indicates an expired or invalid session
|
|
76
|
+
*/
|
|
77
|
+
function isSessionExpiredError(error: string): boolean {
|
|
78
|
+
const lowerError = error.toLowerCase();
|
|
79
|
+
return (
|
|
80
|
+
lowerError.includes("401") ||
|
|
81
|
+
lowerError.includes("403") ||
|
|
82
|
+
lowerError.includes("unauthorized") ||
|
|
83
|
+
lowerError.includes("forbidden") ||
|
|
84
|
+
lowerError.includes("bad authentication") ||
|
|
85
|
+
lowerError.includes("could not authenticate")
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if an error indicates a network problem
|
|
91
|
+
*/
|
|
92
|
+
function isNetworkError(error: string): boolean {
|
|
93
|
+
const lowerError = error.toLowerCase();
|
|
94
|
+
return (
|
|
95
|
+
lowerError.includes("network") ||
|
|
96
|
+
lowerError.includes("enotfound") ||
|
|
97
|
+
lowerError.includes("econnrefused") ||
|
|
98
|
+
lowerError.includes("econnreset") ||
|
|
99
|
+
lowerError.includes("etimedout") ||
|
|
100
|
+
lowerError.includes("fetch failed") ||
|
|
101
|
+
lowerError.includes("socket")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate authentication by resolving credentials and optionally testing them.
|
|
107
|
+
*
|
|
108
|
+
* @param options - Configuration for credential resolution and validation
|
|
109
|
+
* @returns AuthCheckResult with client and user on success, or error details on failure
|
|
110
|
+
*/
|
|
111
|
+
export async function checkAuth(
|
|
112
|
+
options: CheckAuthOptions = {}
|
|
113
|
+
): Promise<AuthCheckResult> {
|
|
114
|
+
const warnings: string[] = [];
|
|
115
|
+
|
|
116
|
+
// Step 1: Resolve credentials from all sources
|
|
117
|
+
const credentialResult = await resolveCredentials({
|
|
118
|
+
authToken: options.authToken,
|
|
119
|
+
ct0: options.ct0,
|
|
120
|
+
cookieSource: options.cookieSource,
|
|
121
|
+
chromeProfile: options.chromeProfile,
|
|
122
|
+
firefoxProfile: options.firefoxProfile,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
warnings.push(...credentialResult.warnings);
|
|
126
|
+
const { cookies } = credentialResult;
|
|
127
|
+
|
|
128
|
+
// Step 2: Check if we have both required cookies
|
|
129
|
+
if (!cookies.authToken || !cookies.ct0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
errorType: "missing_credentials",
|
|
133
|
+
error: ERROR_MESSAGES.missing_credentials,
|
|
134
|
+
warnings,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Step 3: Create the client
|
|
139
|
+
const client = new XClient({
|
|
140
|
+
cookies,
|
|
141
|
+
timeoutMs: options.timeoutMs,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Step 4: Skip validation if requested (useful for faster startup)
|
|
145
|
+
if (options.skipValidation) {
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
client,
|
|
149
|
+
user: { id: "", username: "", name: "" },
|
|
150
|
+
cookies,
|
|
151
|
+
warnings,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Step 5: Validate credentials by fetching current user
|
|
156
|
+
const userResult = await client.getCurrentUser();
|
|
157
|
+
|
|
158
|
+
if (!userResult.success) {
|
|
159
|
+
const error = userResult.error ?? "Unknown error";
|
|
160
|
+
|
|
161
|
+
if (isSessionExpiredError(error)) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
errorType: "expired_session",
|
|
165
|
+
error: ERROR_MESSAGES.expired_session,
|
|
166
|
+
warnings,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isNetworkError(error)) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
errorType: "network_error",
|
|
174
|
+
error: ERROR_MESSAGES.network_error,
|
|
175
|
+
warnings,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
errorType: "unknown",
|
|
182
|
+
error: `${ERROR_MESSAGES.unknown}\n\nDetails: ${error}`,
|
|
183
|
+
warnings,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!userResult.user) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
errorType: "unknown",
|
|
191
|
+
error: ERROR_MESSAGES.unknown,
|
|
192
|
+
warnings,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
client,
|
|
199
|
+
user: userResult.user,
|
|
200
|
+
cookies,
|
|
201
|
+
warnings,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get a user-friendly error message for display
|
|
207
|
+
*/
|
|
208
|
+
export function getAuthErrorMessage(errorType: AuthErrorType): string {
|
|
209
|
+
return ERROR_MESSAGES[errorType];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Format warnings for display to the user
|
|
214
|
+
*/
|
|
215
|
+
export function formatWarnings(warnings: string[]): string {
|
|
216
|
+
if (warnings.length === 0) {
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
return warnings.map((w) => ` - ${w}`).join("\n");
|
|
220
|
+
}
|