blob: 4a3b605f3d9de3ad9bd673318d7faa2247301f32 [file]
/**
* Deciding where imported events should land.
*
* Nothing here is defaulted on purpose: an import can create dozens of events
* at once, so the form makes the reader say which calendar, which project and
* whether they are public before it will save anything.
*/
import { describe, expect, it } from "vitest";
import { anonymousCalendars, makeCalendars } from "../tests/factories";
import {
emptySettings,
importReadiness,
importableCategories,
importableProjects,
importableVisibilities,
looksLikeCalendar,
reconcile,
resolveSettings,
} from "./importing";
import type { ImportSettings } from "./importing";
const committer = makeCalendars({
uid: "alice",
projects: ["httpd", "tomcat"],
committees: [],
can_create: { personal: true, project: ["httpd", "tomcat"], project_private: [], foundation: false },
});
const pmcMember = makeCalendars({
uid: "bob",
projects: ["httpd"],
committees: ["httpd"],
can_create: { personal: true, project: ["httpd"], project_private: ["httpd"], foundation: false },
});
const member = makeCalendars({
uid: "carol",
is_member: true,
projects: ["tomcat"],
committees: ["tomcat"],
can_create: { personal: true, project: ["tomcat"], project_private: ["tomcat"], foundation: true },
});
function settings(overrides: Partial<ImportSettings> = {}): ImportSettings {
return { ...emptySettings(), ...overrides };
}
describe("nothing is chosen to begin with", () => {
it("starts empty", () => {
expect(emptySettings()).toEqual({ category: null, project: null, visibility: null });
});
it("is not ready, and says what is missing", () => {
const readiness = importReadiness(emptySettings(), committer);
expect(readiness.ready).toBe(false);
expect(readiness.missing).toEqual(["which calendar these events belong to"]);
});
});
describe("what is on offer", () => {
it("only the calendars this session may write", () => {
expect(importableCategories(committer)).toEqual(["personal", "project"]);
expect(importableCategories(member)).toEqual(["personal", "project", "foundation"]);
expect(importableCategories(anonymousCalendars)).toEqual([]);
expect(importableCategories(null)).toEqual([]);
});
it("private project events need the committee", () => {
expect(importableProjects(committer, "public")).toEqual(["httpd", "tomcat"]);
expect(importableProjects(committer, "private")).toEqual([]);
expect(importableProjects(pmcMember, "private")).toEqual(["httpd"]);
});
it("personal events have no visibility to choose", () => {
expect(importableVisibilities("personal", committer)).toEqual([]);
});
it("a committer with no committee can only import public project events", () => {
expect(importableVisibilities("project", committer)).toEqual(["public"]);
});
it("a committee member gets both", () => {
expect(importableVisibilities("project", pmcMember)).toEqual(["public", "private"]);
});
it("foundation events are members only", () => {
expect(importableVisibilities("foundation", committer)).toEqual([]);
expect(importableVisibilities("foundation", member)).toEqual(["public", "private"]);
});
});
describe("what still has to be answered", () => {
it("a project import needs a project and a visibility", () => {
const readiness = importReadiness(settings({ category: "project" }), committer);
expect(readiness.ready).toBe(false);
expect(readiness.missing).toEqual([
"whether they are public or private",
"which project they belong to",
]);
});
it("choosing the visibility leaves only the project", () => {
const readiness = importReadiness(
settings({ category: "project", visibility: "public" }),
committer,
);
expect(readiness.missing).toEqual(["which project they belong to"]);
});
it("a fully answered project import is ready", () => {
const readiness = importReadiness(
settings({ category: "project", visibility: "public", project: "httpd" }),
committer,
);
expect(readiness).toEqual({ ready: true, missing: [] });
});
it("a personal import needs nothing more than the calendar", () => {
expect(importReadiness(settings({ category: "personal" }), committer).ready).toBe(true);
});
it("a foundation import still needs a visibility", () => {
expect(importReadiness(settings({ category: "foundation" }), member).ready).toBe(false);
expect(
importReadiness(settings({ category: "foundation", visibility: "private" }), member).ready,
).toBe(true);
});
});
describe("it will not accept an answer the session is not allowed to give", () => {
it("a calendar the user cannot write to", () => {
const readiness = importReadiness(settings({ category: "foundation", visibility: "public" }), committer);
expect(readiness.ready).toBe(false);
expect(readiness.missing).toContain("a calendar you are allowed to add to");
});
it("a project the user is not a member of", () => {
const readiness = importReadiness(
settings({ category: "project", visibility: "public", project: "kafka" }),
committer,
);
expect(readiness.missing).toContain("a project you are allowed to add to");
});
it("a private project event without the committee", () => {
const readiness = importReadiness(
settings({ category: "project", visibility: "private", project: "httpd" }),
committer,
);
expect(readiness.ready).toBe(false);
});
it("...which the committee member may do", () => {
const readiness = importReadiness(
settings({ category: "project", visibility: "private", project: "httpd" }),
pmcMember,
);
expect(readiness.ready).toBe(true);
});
it("anonymous cannot import at all", () => {
expect(importReadiness(settings({ category: "personal" }), anonymousCalendars).ready).toBe(false);
});
});
describe("keeping the form consistent", () => {
it("drops the project when the calendar is not a project one", () => {
const next = reconcile(settings({ category: "personal", project: "httpd" }), committer);
expect(next.project).toBeNull();
});
it("drops the visibility for a personal calendar", () => {
const next = reconcile(settings({ category: "personal", visibility: "public" }), committer);
expect(next.visibility).toBeNull();
});
it("drops a project that switching to private has taken off the list", () => {
const chosen = settings({ category: "project", visibility: "public", project: "tomcat" });
const next = reconcile({ ...chosen, visibility: "private" }, committer);
expect(next.project).toBeNull();
});
it("keeps a project that is still allowed", () => {
const chosen = settings({ category: "project", visibility: "private", project: "httpd" });
expect(reconcile(chosen, pmcMember).project).toBe("httpd");
});
it("drops a visibility the new calendar does not offer", () => {
const chosen = settings({ category: "project", visibility: "private", project: "httpd" });
// A committer switching to a calendar where private is not on offer.
expect(reconcile(chosen, committer).visibility).toBeNull();
});
});
describe("handing the answers to the API", () => {
it("sends project and visibility for a project import", () => {
expect(
resolveSettings(settings({ category: "project", visibility: "public", project: "httpd" })),
).toEqual({ category: "project", visibility: "public", project: "httpd" });
});
it("sends neither for a personal import", () => {
expect(resolveSettings(settings({ category: "personal" }))).toEqual({
category: "personal",
visibility: null,
project: null,
});
});
it("never sends a project on a foundation import", () => {
const resolved = resolveSettings(
settings({ category: "foundation", visibility: "public", project: "httpd" }),
);
expect(resolved.project).toBeNull();
});
it("refuses to resolve before a calendar is chosen", () => {
expect(() => resolveSettings(emptySettings())).toThrow();
});
});
describe("recognising a calendar file", () => {
const asFile = (name: string, type = "") => new File(["BEGIN:VCALENDAR"], name, { type });
it("by extension or by type", () => {
expect(looksLikeCalendar(asFile("events.ics"))).toBe(true);
expect(looksLikeCalendar(asFile("EVENTS.ICS"))).toBe(true);
expect(looksLikeCalendar(asFile("export", "text/calendar"))).toBe(true);
});
it("and not otherwise", () => {
expect(looksLikeCalendar(asFile("holiday.jpg", "image/jpeg"))).toBe(false);
});
});