Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/package-vulnerability-scanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ jobs:
cache-dependency-path: extensions/${{ env.EXTENSION_NAME }}/package-lock.json

- run: npm ci

# Run the frontend unit tests before building.
- name: Run frontend tests
run: npm test

- run: npm run build

# Run the Python backend tests.
Expand Down
6 changes: 3 additions & 3 deletions extensions/package-vulnerability-scanner/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"dist/assets/index-CGfl-rCQ.css": {
"checksum": "b4dadccacaa63c585d549a30e09ed801"
},
"dist/assets/index-B8aNyp2W.js": {
"checksum": "f464a4bc802b89c167439c82979af2d0"
"dist/assets/index-SEZBmp0A.js": {
"checksum": "5045b070467147359ba4430ff5a5d602"
},
"dist/index.html": {
"checksum": "07435c1b16a3ab78d62c07501cc2e32d"
"checksum": "35a9caef18b056356d5caff6e12b3c10"
},
"main.py": {
"checksum": "331f2474111a6280734062de89152bb6"
Expand Down
2 changes: 1 addition & 1 deletion extensions/package-vulnerability-scanner/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const scannerStore = useScannerStore();
const userStore = useUserStore();

// ContentList fetches vulnerabilities after it gathers the installed packages.
userStore.fetchCurrentUser();
userStore.fetchCurrentUser().catch(() => {});
contentStore.fetchContentList();

const loadingMessage = "Fetching your content...";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect } from "vitest";

import { errorDetail } from "./errorDetail";

function response(status: number, body?: unknown): Response {
return {
status,
json: async () => {
if (body === undefined) throw new Error("no body");
return body;
},
} as unknown as Response;
}

describe("errorDetail", () => {
it("returns the backend detail when present", async () => {
const msg = await errorDetail(
response(502, { detail: "Connect API error: nope" }),
"Fallback",
);
expect(msg).toBe("Connect API error: nope");
});

it("falls back to the status when there is no detail field", async () => {
expect(await errorDetail(response(500, {}), "Couldn't load")).toBe(
"Couldn't load (status 500).",
);
});

it("falls back when the body is not JSON", async () => {
expect(await errorDetail(response(503), "Couldn't load")).toBe(
"Couldn't load (status 503).",
);
});
});
16 changes: 16 additions & 0 deletions extensions/package-vulnerability-scanner/src/lib/errorDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Prefer the backend's error `detail` so the user sees the real reason a request
// failed; fall back to a generic message with the HTTP status when there's none.
export async function errorDetail(
response: Response,
fallback: string,
): Promise<string> {
try {
const data = await response.json();
if (data && typeof data.detail === "string" && data.detail) {
return data.detail;
}
} catch {
// No JSON body; use the fallback below.
}
return `${fallback} (status ${response.status}).`;
}
73 changes: 73 additions & 0 deletions extensions/package-vulnerability-scanner/src/stores/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { setActivePinia, createPinia } from "pinia";

import { useUserStore } from "./user";

function mockFetch(response: unknown) {
vi.stubGlobal(
"fetch",
vi.fn(async () => response),
);
}

beforeEach(() => {
setActivePinia(createPinia());
vi.spyOn(console, "error").mockImplementation(() => {});
});

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

describe("user store fetchCurrentUser", () => {
it("sets the user and clears error state on success", async () => {
mockFetch({
ok: true,
status: 200,
json: async () => ({ username: "alice", user_role: "administrator" }),
});
const store = useUserStore();

await store.fetchCurrentUser();

expect(store.user?.username).toBe("alice");
expect(store.error).toBeNull();
expect(store.setupRequired).toBe(false);
expect(store.isAdmin).toBe(true);
});

it("flags setupRequired on a 424 and records the error", async () => {
mockFetch({ ok: false, status: 424 });
const store = useUserStore();

await expect(store.fetchCurrentUser()).rejects.toThrow("424");

expect(store.setupRequired).toBe(true);
expect(store.error?.message).toContain("424");
expect(store.user).toBeUndefined();
});

it("records the error but does not flag setup for a non-424 failure", async () => {
mockFetch({ ok: false, status: 500 });
const store = useUserStore();

await expect(store.fetchCurrentUser()).rejects.toThrow("500");

expect(store.setupRequired).toBe(false);
expect(store.error?.message).toContain("500");
});

it("isAdmin is false for a non-administrator", async () => {
mockFetch({
ok: true,
status: 200,
json: async () => ({ username: "bob", user_role: "publisher" }),
});
const store = useUserStore();

await store.fetchCurrentUser();

expect(store.isAdmin).toBe(false);
});
});
30 changes: 26 additions & 4 deletions extensions/package-vulnerability-scanner/src/stores/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";

import { errorDetail } from "../lib/errorDetail";

export interface User {
email: string;
username: string;
Expand All @@ -17,18 +19,38 @@ export interface User {

export const useUserStore = defineStore("user", () => {
const user = ref<User>();
const error = ref<Error | null>(null);
const setupRequired = ref(false);

const isAdmin = computed(() => user.value?.user_role === "administrator");

async function fetchCurrentUser() {
const response = await fetch("api/user");
const data = await response.json();

user.value = data;
error.value = null;
setupRequired.value = false;
try {
const response = await fetch("api/user");
if (!response.ok) {
// 424: the Connect Visitor API Key integration this app needs is not configured.
if (response.status === 424) setupRequired.value = true;
throw new Error(
await errorDetail(
response,
"Couldn't load your account from Connect",
),
);
}
user.value = await response.json();
} catch (err) {
console.error("Error fetching current user:", err);
error.value = err as Error;
throw err;
}
}

return {
user,
error,
setupRequired,
isAdmin,

fetchCurrentUser,
Expand Down
3 changes: 2 additions & 1 deletion extensions/package-vulnerability-scanner/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.vitest.json" }
]
}
Loading