mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(android upload): handle transient imgur errors
This commit is contained in:
@@ -40,7 +40,7 @@ public class ImgurLiveUploadTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Live provider diagnostic; may fail when Imgur is unavailable from the current network.")
|
@Ignore("Live provider diagnostic; may fail when Imgur is unavailable or temporarily rejecting anonymous uploads.")
|
||||||
public void imgur_liveUpload_fromGeneratedPng_succeeds() throws Exception {
|
public void imgur_liveUpload_fromGeneratedPng_succeeds() throws Exception {
|
||||||
Intent launchIntent = new Intent(appContext, MainActivity.class);
|
Intent launchIntent = new Intent(appContext, MainActivity.class);
|
||||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public class MediaUploadAutomationRunner {
|
|||||||
static final String STAGE_SUBMIT_CLICKED = "submit_clicked";
|
static final String STAGE_SUBMIT_CLICKED = "submit_clicked";
|
||||||
static final String STAGE_SUCCESS_SELECTOR_MATCHED = "success_selector_matched";
|
static final String STAGE_SUCCESS_SELECTOR_MATCHED = "success_selector_matched";
|
||||||
static final String STAGE_BLOCKED_DETECTED = "blocked_detected";
|
static final String STAGE_BLOCKED_DETECTED = "blocked_detected";
|
||||||
|
static final String STAGE_PROVIDER_ERROR = "provider_error";
|
||||||
static final String STAGE_INPUT_NOT_FOUND = "input_not_found";
|
static final String STAGE_INPUT_NOT_FOUND = "input_not_found";
|
||||||
static final String STAGE_CHOOSER_NOT_TRIGGERED = "chooser_not_triggered";
|
static final String STAGE_CHOOSER_NOT_TRIGGERED = "chooser_not_triggered";
|
||||||
static final String STAGE_FILE_PAYLOAD_UNAVAILABLE = "file_payload_unavailable";
|
static final String STAGE_FILE_PAYLOAD_UNAVAILABLE = "file_payload_unavailable";
|
||||||
@@ -500,6 +501,7 @@ public class MediaUploadAutomationRunner {
|
|||||||
|
|
||||||
String successJs = MediaUploadRecipes.getSuccessJs(provider);
|
String successJs = MediaUploadRecipes.getSuccessJs(provider);
|
||||||
String blockedJs = MediaUploadRecipes.getBlockedJs(provider);
|
String blockedJs = MediaUploadRecipes.getBlockedJs(provider);
|
||||||
|
String providerErrorJs = MediaUploadRecipes.getProviderErrorJs(provider);
|
||||||
if (successJs == null || blockedJs == null) {
|
if (successJs == null || blockedJs == null) {
|
||||||
finish(
|
finish(
|
||||||
new MediaUploadResult(
|
new MediaUploadResult(
|
||||||
@@ -523,6 +525,39 @@ public class MediaUploadAutomationRunner {
|
|||||||
null));
|
null));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (providerErrorJs != null) {
|
||||||
|
webView.evaluateJavascript(
|
||||||
|
providerErrorJs,
|
||||||
|
providerError -> {
|
||||||
|
if (finished) return;
|
||||||
|
String cleaned =
|
||||||
|
providerError == null
|
||||||
|
? ""
|
||||||
|
: providerError
|
||||||
|
.replaceAll("^\"|\"$", "")
|
||||||
|
.replace("\\u003d", "=")
|
||||||
|
.replace("\\\"", "\"")
|
||||||
|
.trim();
|
||||||
|
if (!cleaned.isEmpty() && !"null".equals(cleaned)) {
|
||||||
|
finish(
|
||||||
|
new MediaUploadResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"Provider error: " + cleaned,
|
||||||
|
STAGE_PROVIDER_ERROR,
|
||||||
|
elapsedMs(),
|
||||||
|
lastMatchedSelector));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evaluateSuccess(successJs);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evaluateSuccess(successJs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateSuccess(String successJs) {
|
||||||
webView.evaluateJavascript(
|
webView.evaluateJavascript(
|
||||||
successJs,
|
successJs,
|
||||||
url -> {
|
url -> {
|
||||||
@@ -538,7 +573,6 @@ public class MediaUploadAutomationRunner {
|
|||||||
}
|
}
|
||||||
schedulePoll();
|
schedulePoll();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finish(MediaUploadResult result) {
|
private void finish(MediaUploadResult result) {
|
||||||
|
|||||||
@@ -339,6 +339,21 @@ public final class MediaUploadRecipes {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS to detect visible provider-side upload failures that are not CAPTCHA/login blocks.
|
||||||
|
* Returns an error string when a known provider failure is present, null otherwise.
|
||||||
|
*/
|
||||||
|
public static String getProviderErrorJs(String provider) {
|
||||||
|
if (PROVIDER_IMGUR.equals(provider)) {
|
||||||
|
return "(function(){"
|
||||||
|
+ "var text=((document.body&&document.body.innerText)||'');"
|
||||||
|
+ "if(text.indexOf('CREATE_ALBUM_FAIL')!==-1)return 'CREATE_ALBUM_FAIL';"
|
||||||
|
+ "if(text.indexOf('Something went wrong')!==-1)return 'Something went wrong';"
|
||||||
|
+ "return null;})()";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String buildBlockedJs(String[] selectors) {
|
private static String buildBlockedJs(String[] selectors) {
|
||||||
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
||||||
for (int i = 0; i < selectors.length; i++) {
|
for (int i = 0; i < selectors.length; i++) {
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ public class MediaUploadRecipesTest {
|
|||||||
assertNull(MediaUploadRecipes.getUploadUrl(null));
|
assertNull(MediaUploadRecipes.getUploadUrl(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isAndroidUploadProvider_includesAndroidUploadProviders() {
|
||||||
|
assertTrue(MediaUploadRecipes.isAndroidUploadProvider(MediaUploadRecipes.PROVIDER_IMGUR));
|
||||||
|
assertTrue(MediaUploadRecipes.isAndroidUploadProvider(MediaUploadRecipes.PROVIDER_IMGBB));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getTriggerFileInputJs_imgur_containsSelectors() {
|
public void getTriggerFileInputJs_imgur_containsSelectors() {
|
||||||
String js = MediaUploadRecipes.getTriggerFileInputJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
String js = MediaUploadRecipes.getTriggerFileInputJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
||||||
@@ -110,6 +116,13 @@ public class MediaUploadRecipesTest {
|
|||||||
assertTrue(js.contains("challenge") || js.contains("captcha") || js.contains("recaptcha"));
|
assertTrue(js.contains("challenge") || js.contains("captcha") || js.contains("recaptcha"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderErrorJs_imgur_detectsCreateAlbumFail() {
|
||||||
|
String js = MediaUploadRecipes.getProviderErrorJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
||||||
|
assertNotNull(js);
|
||||||
|
assertTrue(js.contains("CREATE_ALBUM_FAIL"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void failureClassification_inputNotFound_stageConstant() {
|
public void failureClassification_inputNotFound_stageConstant() {
|
||||||
assertEquals(MediaUploadAutomationRunner.STAGE_INPUT_NOT_FOUND, "input_not_found");
|
assertEquals(MediaUploadAutomationRunner.STAGE_INPUT_NOT_FOUND, "input_not_found");
|
||||||
@@ -127,6 +140,11 @@ public class MediaUploadRecipesTest {
|
|||||||
assertEquals(MediaUploadAutomationRunner.STAGE_BLOCKED_DETECTED, "blocked_detected");
|
assertEquals(MediaUploadAutomationRunner.STAGE_BLOCKED_DETECTED, "blocked_detected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failureClassification_providerError_stageConstant() {
|
||||||
|
assertEquals(MediaUploadAutomationRunner.STAGE_PROVIDER_ERROR, "provider_error");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void failureClassification_uploadTimedOut_stageConstant() {
|
public void failureClassification_uploadTimedOut_stageConstant() {
|
||||||
assertEquals(MediaUploadAutomationRunner.STAGE_UPLOAD_TIMED_OUT, "upload_timed_out");
|
assertEquals(MediaUploadAutomationRunner.STAGE_UPLOAD_TIMED_OUT, "upload_timed_out");
|
||||||
|
|||||||
@@ -442,11 +442,41 @@ describe('useFileUpload', () => {
|
|||||||
});
|
});
|
||||||
const call = vi.mocked(FileUploader.pickAndUploadMedia).mock.calls[0][0];
|
const call = vi.mocked(FileUploader.pickAndUploadMedia).mock.calls[0][0];
|
||||||
expect(call?.providerOrder).toEqual(expect.arrayContaining(['catbox']));
|
expect(call?.providerOrder).toEqual(expect.arrayContaining(['catbox']));
|
||||||
expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgur']));
|
|
||||||
expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgbb']));
|
expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgbb']));
|
||||||
|
expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgur']));
|
||||||
expect(onUploadComplete).toHaveBeenCalledWith('https://files.catbox.moe/random.jpg', 'random.jpg');
|
expect(onUploadComplete).toHaveBeenCalledWith('https://files.catbox.moe/random.jpg', 'random.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces Android provider errors with their stage', async () => {
|
||||||
|
uploadModeRef.value = 'random';
|
||||||
|
preferredProviderRef.value = 'catbox';
|
||||||
|
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
|
||||||
|
const error = new Error('All providers failed') as Error & { data: { attempts: unknown[] } };
|
||||||
|
error.data = {
|
||||||
|
attempts: [
|
||||||
|
{
|
||||||
|
provider: 'imgur',
|
||||||
|
success: false,
|
||||||
|
error: 'Provider error: CREATE_ALBUM_FAIL',
|
||||||
|
stage: 'provider_error',
|
||||||
|
elapsedMs: 2200,
|
||||||
|
matchedSelectors: '#file-input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
vi.mocked(FileUploader.pickAndUploadMedia).mockRejectedValue(error);
|
||||||
|
const { onUploadComplete, hook } = mountHook();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await hook().handleUpload();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.alert).toHaveBeenCalledWith(
|
||||||
|
'upload_failed. upload_failed_all_providers: imgur: Provider error: CREATE_ALBUM_FAIL (stage=provider_error, 2200ms, tried=[#file-input])',
|
||||||
|
);
|
||||||
|
expect(onUploadComplete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('random mode: Electron upload succeeds when orchestrator resolves', async () => {
|
it('random mode: Electron upload succeeds when orchestrator resolves', async () => {
|
||||||
uploadModeRef.value = 'random';
|
uploadModeRef.value = 'random';
|
||||||
preferredProviderRef.value = 'catbox';
|
preferredProviderRef.value = 'catbox';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const ANDROID_STAGE_MAP: Record<string, UploadAttemptStage> = {
|
|||||||
chooser_not_triggered: 'file_input',
|
chooser_not_triggered: 'file_input',
|
||||||
upload_timed_out: 'timeout',
|
upload_timed_out: 'timeout',
|
||||||
blocked_detected: 'blocked',
|
blocked_detected: 'blocked',
|
||||||
|
provider_error: 'provider_error',
|
||||||
no_recipe: 'unknown',
|
no_recipe: 'unknown',
|
||||||
page_loaded: 'page_load',
|
page_loaded: 'page_load',
|
||||||
blocked: 'blocked',
|
blocked: 'blocked',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type MediaHostingRuntime = 'web' | 'electron' | 'android';
|
|||||||
/** Stage at which an upload attempt failed (enables comparable errors across Electron/Android) */
|
/** Stage at which an upload attempt failed (enables comparable errors across Electron/Android) */
|
||||||
export type UploadAttemptStage =
|
export type UploadAttemptStage =
|
||||||
| 'blocked' /** captcha/login/challenge detected */
|
| 'blocked' /** captcha/login/challenge detected */
|
||||||
|
| 'provider_error' /** provider returned an upload-specific error */
|
||||||
| 'file_input' /** file input not found */
|
| 'file_input' /** file input not found */
|
||||||
| 'submit' /** submit button not found */
|
| 'submit' /** submit button not found */
|
||||||
| 'timeout' /** upload or extraction timeout */
|
| 'timeout' /** upload or extraction timeout */
|
||||||
|
|||||||
Reference in New Issue
Block a user