openflexure-microscope-server/webapp/src/tests/unit/stores/wot.spec.js

244 lines
8.7 KiB
JavaScript

import { setActivePinia, createPinia } from "pinia";
import { describe, it, expect, beforeEach, vi } from "vitest";
import axios from "axios";
import { useWotStore, findFormHref } from "../../../stores/wot";
// Mock Axios
vi.mock("axios");
describe("WoT Store", () => {
// A reusable dummy Thing Description (TD)
// This will be reworked by using the HAR fixtures or the fixtures/common_definitions.js
// This test uses arbitrary values for simplicity but this could add confusion
// How-to-test documentation would require a glossary of defined mock values
const mockTD = {
base: "http://microscope.local/api/v3/",
properties: {
status: {
forms: [{ op: ["readproperty", "writeproperty"], href: "status" }],
},
},
actions: {
toggle: {
forms: [{ op: "invokeaction", href: "http://external.com/api/v3/toggle" }],
},
},
};
beforeEach(() => {
// New Pinia for every test to avoid leaks and inter-playing states
// Clear axios call history between tests
setActivePinia(createPinia());
vi.clearAllMocks();
});
describe("Standalone Helper: findFormHref", () => {
it("returns undefined if affordance is missing", () => {
expect(findFormHref(undefined, "read")).toBeUndefined();
});
it("finds href when op is an array", () => {
const affordance = mockTD.properties.status;
expect(findFormHref(affordance, "readproperty")).toBe("status");
});
it("finds href when op is a string", () => {
const affordance = mockTD.actions.toggle;
expect(findFormHref(affordance, "invokeaction")).toBe("http://external.com/api/v3/toggle");
});
it("returns undefined if op does not match", () => {
const affordance = mockTD.properties.status;
expect(findFormHref(affordance, "fakeoperation")).toBeUndefined();
});
});
describe("State & Basic Actions", () => {
it("adds, removes, and clears thing descriptions", () => {
const store = useWotStore();
store.addThingDescription("camera", mockTD);
expect(store.thingDescriptions["camera"]).toEqual(mockTD);
expect(store.thingList).toContain("camera");
store.removeThingDescription("camera");
expect(store.thingDescriptions["camera"]).toBeUndefined();
expect(store.thingList.length).toBe(0);
// Test clear all
store.addThingDescription("cam1", mockTD);
store.addThingDescription("cam2", mockTD);
store.removeAllThingDescriptions();
expect(store.thingList.length).toBe(0);
});
it("correctly identifies if a thing is available", () => {
const store = useWotStore();
store.addThingDescription("camera", mockTD);
expect(store.thingAvailable("camera")).toBe(true);
expect(store.thingAvailable("stage")).toBe(false);
});
});
describe("Network Actions (Axios)", () => {
it("fetches a single Thing Description WITH a provided name", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({ data: mockTD });
await store.fetchThingDescription("http://microscope.local/api/v3/camera", "myCamera");
expect(axios.get).toHaveBeenCalledWith("http://microscope.local/api/v3/camera");
expect(store.thingDescriptions["myCamera"]).toEqual(mockTD);
});
it("fetches a single Thing Description and extracts name from URI if missing", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({ data: mockTD });
// The logic should split by "/" and grab "stage"
await store.fetchThingDescription("http://microscope.local/api/v3/stage/");
expect(store.thingDescriptions["stage"]).toEqual(mockTD);
});
it("fetches multiple Thing Descriptions", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({
status: 200,
data: {
"/sensor/": { title: "Sensor TD" },
motor: { title: "Motor TD" },
},
});
await store.fetchThingDescriptions("http://microscope.local/api/v3/things");
// The logic strips slashes: "/sensor/" -> "sensor"
expect(store.thingDescriptions["sensor"].title).toBe("Sensor TD");
expect(store.thingDescriptions["motor"].title).toBe("Motor TD");
});
it("throws an error if fetchThingDescriptions fails", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({ status: 404 });
await expect(store.fetchThingDescriptions("http://bad-url")).rejects.toMatch(
"Could not retrieve",
);
});
});
describe("TD Parsing & URL Generation", () => {
beforeEach(() => {
const store = useWotStore();
const mockTD = {
base: "http://microscope.local/api/v3/",
properties: {
status: {
forms: [{ op: ["readproperty", "writeproperty"], href: "status" }],
},
},
actions: {
toggle: {
forms: [{ op: "invokeaction", href: "http://external.com/api/v3/toggle" }],
},
},
};
store.addThingDescription("microscope", mockTD);
});
it("checks affordance availability (and covers Line 55)", () => {
const store = useWotStore();
// Success case
expect(store.thingAffordanceAvailable("microscope", "properties", "status")).toBe(true);
// Missing Thing (!td)
expect(store.thingAffordanceAvailable("fakeThing", "properties", "status")).toBe(false);
// Missing Property inside an existing Category
expect(store.thingAffordanceAvailable("microscope", "properties", "fakeProp")).toBe(false);
// Thing exists, but the Category/AffordanceType ('events') does not exist at all!
expect(store.thingAffordanceAvailable("microscope", "events", "someEvent")).toBe(false);
});
it("handles completely missing forms, affordances, and operations", () => {
const store = useWotStore();
// Missing Thing entirely
expect(
store.thingFormUrl("fakeThing", "properties", "missing", "read", true),
).toBeUndefined();
expect(() => {
store.thingFormUrl("fakeThing", "properties", "missing", "read", false);
}).toThrowError(/Could not find form/);
// The 'microscope' exists, but 'fakeProp' does not.
expect(
store.thingFormUrl("microscope", "properties", "fakeProp", "read", true),
).toBeUndefined();
expect(() => {
store.thingFormUrl("microscope", "properties", "fakeProp", "read", false);
}).toThrowError(/Could not find form/);
// 'microscope' and 'status' exist, but 'fakeOp' is missing.
expect(
store.thingFormUrl("microscope", "properties", "status", "fakeOp", true),
).toBeUndefined();
expect(() => {
store.thingFormUrl("microscope", "properties", "status", "fakeOp", false);
}).toThrowError(/Could not find form/);
});
it("thingFormUrl concats base url with relative href", () => {
const store = useWotStore();
const url = store.thingFormUrl("microscope", "properties", "status", "readproperty");
expect(url).toBe("http://microscope.local/api/v3/status");
});
it("thingFormUrl uses absolute href directly if it starts with http", () => {
const store = useWotStore();
const url = store.thingFormUrl("microscope", "actions", "toggle", "invokeaction");
expect(url).toBe("http://external.com/api/v3/toggle");
});
it("returns relative href if the Thing Description has no base property", () => {
const store = useWotStore();
store.addThingDescription("noBaseThing", {
properties: {
status: { forms: [{ op: "readproperty", href: "relative-path-only" }] },
},
});
const url = store.thingFormUrl("noBaseThing", "properties", "status", "readproperty");
expect(url).toBe("relative-path-only");
});
it("normalizes slashes when base lacks trailing slash and href has leading slash", () => {
const store = useWotStore();
store.addThingDescription("messyThing", {
// No trailing slash
base: "http://microscope.local/api/v3",
properties: {
// Extra leading slash
status: { forms: [{ op: "readproperty", href: "/status" }] },
},
});
const url = store.thingFormUrl("messyThing", "properties", "status", "readproperty");
expect(url).toBe("http://microscope.local/api/v3/status");
});
it("thingPropertyUrl and thingActionUrl proxy correctly", () => {
const store = useWotStore();
expect(store.thingPropertyUrl("microscope", "status", "readproperty")).toBe(
"http://microscope.local/api/v3/status",
);
expect(store.thingActionUrl("microscope", "toggle", "invokeaction")).toBe(
"http://external.com/api/v3/toggle",
);
});
});
});