fix(tauri-app): Token an die AppView senden — und die Erneuerung reparieren
Die vier viewer-bezogenen AppView-Aufrufe (Timeline, Notifications, Count, Seen) senden jetzt das Access-JWT. Ohne Session gibt es einen sprechenden Fehler statt eines leeren Bearer-Headers. Dabei kam heraus, dass die automatische Token-Erneuerung noch nie funktioniert hat: isTokenInvalid() stieg mit `typeof e !== "object"` sofort aus, aber Tauri lehnt bei Commands mit Result<T, String> mit einem blanken String ab — der Zweig war seit seiner Einführung tot. Belegt per Mutationstest: mit der alten Zeile fallen acht der neuen Tests um. Die Prüfung liest den Fehlertext jetzt über einen Helfer, der Strings und Objekte behandelt. Dazu: der Badge-Poll bricht ab, wenn die Erneuerung endgültig scheitert, statt weiter gegen einen 401 zu laufen. 503 AuthUnavailable gilt dabei bewusst nicht als Auth-Fehler — die PDS kann kurz weg sein, der Poll soll das überdauern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
ac18ff7a16
commit
9ee717bbc7
@@ -2,6 +2,7 @@
|
||||
import Avatar from "./Avatar.svelte";
|
||||
import Skeleton from "./Skeleton.svelte";
|
||||
import {
|
||||
errorMessage,
|
||||
fetchNotifications,
|
||||
markNotificationsSeen,
|
||||
notificationIcon,
|
||||
@@ -71,7 +72,7 @@
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
error = errorMessage(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -89,7 +90,7 @@
|
||||
items = [...items, ...r.notifications.filter((n) => !seen.has(n.id))];
|
||||
cursor = r.cursor;
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
error = errorMessage(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -194,6 +194,48 @@ describe("NotificationsView actor navigation", () => {
|
||||
expect(onThreadClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows actionable copy when the AppView rejects the session", async () => {
|
||||
// Since `/api/notifications` grew an auth guard, this is what a
|
||||
// rejected token looks like by the time it reaches the view: the
|
||||
// AppView's JSON body, wrapped by `appview_client.rs`'s
|
||||
// `status_error()` and stringified across the Tauri IPC boundary.
|
||||
// `safeInvoke` has already spent its one refresh attempt getting
|
||||
// here, so the only thing left to tell the user is "log in again" —
|
||||
// rendering the raw wire string would be accurate and useless.
|
||||
fetchNotificationsMock.mockRejectedValue(
|
||||
'appview: notifications returned 401 Unauthorized: ' +
|
||||
'{"error":"TokenInvalid","message":"ExpiredSignature"}',
|
||||
);
|
||||
|
||||
app = mount(NotificationsView, {
|
||||
target,
|
||||
props: { did: "did:plc:me" },
|
||||
});
|
||||
await flush();
|
||||
|
||||
expect(target.textContent).toContain("bitte neu anmelden");
|
||||
expect(target.textContent).not.toContain("TokenInvalid");
|
||||
expect(target.textContent).not.toContain("401");
|
||||
// A failed load must not leave the spinner up or ack a page it
|
||||
// never rendered.
|
||||
expect(markNotificationsSeenMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still shows a server error verbatim — there's nothing better to say", async () => {
|
||||
fetchNotificationsMock.mockRejectedValue(
|
||||
"appview: notifications returned 500 Internal Server Error: db down",
|
||||
);
|
||||
|
||||
app = mount(NotificationsView, {
|
||||
target,
|
||||
props: { did: "did:plc:me" },
|
||||
});
|
||||
await flush();
|
||||
|
||||
expect(target.textContent).toContain("500");
|
||||
expect(target.textContent).toContain("db down");
|
||||
});
|
||||
|
||||
it("opens the thread for a row that has a subject", async () => {
|
||||
fetchNotificationsMock.mockResolvedValue({
|
||||
notifications: [row()],
|
||||
|
||||
Reference in New Issue
Block a user