fix(FN-2313): make PR auth checks gh-first across settings UI
- Replace the server-computed settings flag with prAuthAvailable and document it in shared Settings types - Compute PR auth availability from gh CLI auth first, with GITHUB_TOKEN as fallback in GET /api/settings - Rename dashboard settings state/props/hooks from githubTokenConfigured to prAuthAvailable and strip server-owned auth fields on save - Update PR section messaging to guide users to run 'gh auth login' and refresh related route/component tests
This commit is contained in:
@@ -35,6 +35,7 @@ vi.mock("@fusion/core", async () => {
|
||||
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
|
||||
return {
|
||||
...actual,
|
||||
isGhAvailable: vi.fn(),
|
||||
isGhAuthenticated: vi.fn(),
|
||||
isQmdAvailable: vi.fn().mockResolvedValue(false),
|
||||
CentralCore: vi.fn().mockImplementation(() => ({
|
||||
@@ -84,8 +85,9 @@ vi.mock("@fusion/engine", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { isGhAuthenticated } from "@fusion/core";
|
||||
import { isGhAvailable, isGhAuthenticated } from "@fusion/core";
|
||||
|
||||
const mockIsGhAvailable = vi.mocked(isGhAvailable);
|
||||
const mockIsGhAuthenticated = vi.mocked(isGhAuthenticated);
|
||||
|
||||
function createMockGlobalSettingsStore() {
|
||||
@@ -12269,6 +12271,8 @@ describe("GET /settings", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockStore();
|
||||
mockIsGhAvailable.mockReturnValue(false);
|
||||
mockIsGhAuthenticated.mockReturnValue(false);
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
@@ -12290,16 +12294,33 @@ describe("GET /settings", () => {
|
||||
expect(res.body.pollIntervalMs).toBe(DEFAULT_SETTINGS.pollIntervalMs);
|
||||
});
|
||||
|
||||
it("injects githubTokenConfigured as true when token is configured", async () => {
|
||||
it("injects prAuthAvailable as true when gh is available and authenticated", async () => {
|
||||
mockIsGhAvailable.mockReturnValue(true);
|
||||
mockIsGhAuthenticated.mockReturnValue(true);
|
||||
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue(DEFAULT_SETTINGS);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store)); // no githubToken option
|
||||
|
||||
const res = await GET(app, "/api/settings");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.prAuthAvailable).toBe(true);
|
||||
expect(res.body.githubTokenConfigured).toBeUndefined();
|
||||
});
|
||||
|
||||
it("injects prAuthAvailable as true when token fallback is configured", async () => {
|
||||
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue(DEFAULT_SETTINGS);
|
||||
|
||||
const res = await GET(buildApp(), "/api/settings");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.githubTokenConfigured).toBe(true);
|
||||
expect(res.body.prAuthAvailable).toBe(true);
|
||||
expect(res.body.githubTokenConfigured).toBeUndefined();
|
||||
});
|
||||
|
||||
it("injects githubTokenConfigured as false when no token", async () => {
|
||||
it("injects prAuthAvailable as false when neither gh auth nor token fallback is available", async () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store)); // no githubToken option
|
||||
@@ -12309,7 +12330,8 @@ describe("GET /settings", () => {
|
||||
const res = await GET(app, "/api/settings");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.githubTokenConfigured).toBe(false);
|
||||
expect(res.body.prAuthAvailable).toBe(false);
|
||||
expect(res.body.githubTokenConfigured).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns 500 on store error", async () => {
|
||||
@@ -12352,7 +12374,7 @@ describe("PUT /settings", () => {
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({ maxConcurrent: 8 });
|
||||
});
|
||||
|
||||
it("strips server-owned fields (githubTokenConfigured) before calling store.updateSettings", async () => {
|
||||
it("strips server-owned fields before calling store.updateSettings", async () => {
|
||||
const updatedSettings = { ...DEFAULT_SETTINGS, maxConcurrent: 4 };
|
||||
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
|
||||
|
||||
@@ -12360,12 +12382,12 @@ describe("PUT /settings", () => {
|
||||
buildApp(),
|
||||
"PUT",
|
||||
"/api/settings",
|
||||
JSON.stringify({ maxConcurrent: 4, githubTokenConfigured: true }),
|
||||
JSON.stringify({ maxConcurrent: 4, githubTokenConfigured: true, prAuthAvailable: true }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// The server should strip githubTokenConfigured before passing to store
|
||||
// The server should strip server-computed fields before passing to store
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({ maxConcurrent: 4 });
|
||||
});
|
||||
|
||||
@@ -12373,12 +12395,11 @@ describe("PUT /settings", () => {
|
||||
const updatedSettings = { ...DEFAULT_SETTINGS, maxWorktrees: 10 };
|
||||
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
|
||||
|
||||
// Currently only githubTokenConfigured is server-owned
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"PUT",
|
||||
"/api/settings",
|
||||
JSON.stringify({ maxWorktrees: 10, githubTokenConfigured: true }),
|
||||
JSON.stringify({ maxWorktrees: 10, githubTokenConfigured: true, prAuthAvailable: true }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
|
||||
@@ -2391,10 +2391,11 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const settings = await scopedStore.getSettingsFast();
|
||||
const prAuthAvailable = (isGhAvailable() && isGhAuthenticated()) || Boolean(githubToken);
|
||||
// Inject server-side configuration flags
|
||||
res.json({
|
||||
...settings,
|
||||
githubTokenConfigured: Boolean(githubToken),
|
||||
prAuthAvailable,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
@@ -2410,7 +2411,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Strip server-owned fields that should never be persisted to config.json.
|
||||
// These are computed server-side and injected only on GET /settings.
|
||||
|
||||
const { githubTokenConfigured, ...clientSettings } = req.body;
|
||||
const { githubTokenConfigured, prAuthAvailable, ...clientSettings } = req.body;
|
||||
|
||||
// Reject global-only fields with a helpful error pointing to the correct endpoint
|
||||
const globalKeySet = new Set<string>(GLOBAL_SETTINGS_KEYS);
|
||||
@@ -3301,7 +3302,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
/**
|
||||
* GET /api/settings/global
|
||||
* Returns the global (user-level) settings from ~/.fusion/settings.json.
|
||||
* Does NOT include computed/server-only fields like githubTokenConfigured.
|
||||
* Does NOT include computed/server-only fields like prAuthAvailable.
|
||||
*/
|
||||
router.get("/settings/global", async (_req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user