Skip to content

Commit

Permalink
refactor: event form and api routes
Browse files Browse the repository at this point in the history
  • Loading branch information
lowercasename committed Oct 7, 2023
1 parent 9341659 commit b795d07
Show file tree
Hide file tree
Showing 24 changed files with 1,634 additions and 1,228 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
with:
start: pnpm start
browser: chrome

- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
Expand Down
160 changes: 127 additions & 33 deletions cypress/e2e/event.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const eventData = {
timezone: "Europe/London",
eventDescription: "Event Description",
eventURL: "https://example.com",
imagePath: "path/to/your/image.jpg", // If you have an image to upload
hostName: "Your Name",
creatorEmail: "[email protected]",
eventGroupCheckbox: false,
Expand All @@ -14,40 +13,25 @@ const eventData = {
joinCheckbox: true,
maxAttendeesCheckbox: true,
maxAttendees: 10,
eventStart: "",
eventEnd: "",
eventStart: "2030-01-01T00:00",
eventEnd: "2030-01-01T01:00",
};

describe("Events", () => {
beforeEach(() => {
cy.clearLocalStorage();

cy.visit("/new");
cy.get("#showNewEventFormButton").click();

cy.get("#eventName").type(eventData.eventName);
cy.get("#eventLocation").type(eventData.eventLocation);
cy.get("#eventStart").click();
// This opens a datepicker, so find the first non-disabled day and click it
cy.get(".datepicker--cell-day:not(.-disabled-)").first().click();
cy.get("#eventStart").invoke("val").as("eventStart");
// Click away from the datepicker to close it
cy.get("#eventName").click();
cy.get("#eventEnd").click();
// This opens a datepicker, so find the last non-disabled day and click it
cy.get(".datepicker--cell-day:not(.-disabled-)").last().click();
cy.get("#eventEnd").invoke("val").as("eventEnd");
// Click away from the datepicker to close it
cy.get("#eventName").click();
// These are datetime-local inputs
cy.get("#eventStart").type(eventData.eventStart);
cy.get("#eventEnd").type(eventData.eventEnd);
// #timezone is a Select2 dropdown, so select the option you want
cy.get("#timezone").select(eventData.timezone, { force: true });

cy.get("#eventDescription").type(eventData.eventDescription);
cy.get("#eventURL").type(eventData.eventURL);
// Upload an image
// if (eventData.imagePath) {
// cy.get("#eventImageUpload").attachFile(eventData.imagePath);
// }

cy.get("#hostName").type(eventData.hostName);
cy.get("#creatorEmail").type(eventData.creatorEmail);
Expand All @@ -74,6 +58,16 @@ describe("Events", () => {

// Submit the form
cy.get("#newEventFormSubmit").click();

// Wait for the new page to load
cy.url().should("not.include", "/new");

// Get the new event ID from the URL
cy.url().then((url) => {
const [eventID, editToken] = url.split("/").pop().split("?");
cy.wrap(eventID).as("eventID");
cy.wrap(editToken).as("editToken");
});
});
it("creates a new event", function () {
// Check that all the data is correct
Expand All @@ -82,30 +76,25 @@ describe("Events", () => {
cy.get(".p-summary").should("contain.text", eventData.eventDescription);
cy.get("#hosted-by").should(
"contain.text",
`Hosted by ${eventData.hostName}`
`Hosted by ${eventData.hostName}`,
);
cy.get("#attendees-alert").should("contain.text", "10 spots remaining");
let [startDate, startTime] = this.eventStart.split(", ");
let [endDate, endTime] = this.eventEnd.split(", ");
// Remove leading zeroes from the times
startTime = startTime.replace(/^0+/, "");
endTime = endTime.replace(/^0+/, "");
cy.get(".dt-duration").should("contain.text", startDate);
cy.get(".dt-duration").should("contain.text", endDate);
cy.get(".dt-duration").should("contain.text", startTime);
cy.get(".dt-duration").should("contain.text", endTime);
cy.get(".dt-duration").should(
"contain.text",
"Tuesday 1 January 2030 from 12:00 am to 1:00 am (GMT)",
);
});

it("allows you to attend an event", function () {
cy.get("button#attendEvent").click();
cy.get("#attendeeName").type("Test Attendee");
cy.get("#attendeeNumber").clear();
cy.get("#attendeeNumber").focus().clear();
cy.get("#attendeeNumber").type("2");
cy.get("form#attendEventForm").submit();
cy.get("#attendees-alert").should("contain.text", "8 spots remaining");
cy.get(".attendeesList").should(
"contain.text",
"Test Attendee (2 people)"
"Test Attendee (2 people)",
);
});

Expand All @@ -116,4 +105,109 @@ describe("Events", () => {
cy.get(".comment").should("contain.text", "Test Author");
cy.get(".comment").should("contain.text", "Test Comment");
});

it("displays the ActivityPub featured post", function () {
cy.log(this.eventID);

cy.request({
url: `/${this.eventID}/featured`,
headers: {
Accept: "application/activity+json",
},
}).then((response) => {
expect(response.body).to.have.property("@context");
expect(response.body).to.have.property("id");
expect(response.body).to.have.property("type");
expect(response.body).to.have.property("orderedItems");
expect(response.body.orderedItems)
.to.be.an("array")
.and.to.have.lengthOf(1);
const featuredPost = response.body.orderedItems[0];
expect(featuredPost).to.have.property("@context");
expect(featuredPost).to.have.property("id");
expect(featuredPost).to.have.property("type");
expect(featuredPost).to.have.property("name");
expect(featuredPost).to.have.property("content");
expect(featuredPost).to.have.property("attributedTo");
});
});

it("responds correctly to ActivityPub webfinger requests", function () {
cy.request({
url: `/.well-known/webfinger?resource=acct:${
this.eventID
}@${Cypress.env("CYPRESS_DOMAIN")}`,
headers: {
Accept: "application/activity+json",
},
}).then((response) => {
expect(response.body).to.have.property("subject");
expect(response.body).to.have.property("links");
expect(response.body.links)
.to.be.an("array")
.and.to.have.lengthOf(1);
const link = response.body.links[0];
expect(link).to.have.property("rel");
expect(link).to.have.property("type");
expect(link).to.have.property("href");
});
});

it("edits an event", function () {
cy.get("#editEvent").click();

// The edit form is the same as the new form, so we can just re-use the same selectors
// but we need to clear the fields first
cy.get("#editEventForm #eventName").focus().clear();
cy.get("#editEventForm #eventLocation").focus().clear();
cy.get("#editEventForm #eventStart").focus().clear();
cy.get("#editEventForm #eventEnd").focus().clear();
cy.get("#editEventForm #eventDescription").focus().clear();
cy.get("#editEventForm #eventURL").focus().clear();
cy.get("#editEventForm #hostName").focus().clear();
cy.get("#editEventForm #creatorEmail").focus().clear();
cy.get("#editEventForm #maxAttendees").focus().clear();

cy.get("#editEventForm #eventName").type("Edited Event Name");
cy.get("#editEventForm #eventLocation").type("Edited Event Location");
// These are datetime-local inputs
cy.get("#editEventForm #eventStart").type("2030-12-01T00:00");
cy.get("#editEventForm #eventEnd").type("2030-12-01T01:00");
// #timezone is a Select2 dropdown, so select the option you want
cy.get("#editEventForm #timezone").select("Australia/Sydney", {
force: true,
});
cy.get("#editEventForm #eventDescription").type(
"Edited Event Description",
);
cy.get("#editEventForm #eventURL").type("https://edited.example.com");
cy.get("#editEventForm #hostName").type("Edited Name");
cy.get("#editEventForm #creatorEmail").type("[email protected]");

cy.get("#editEventForm #maxAttendeesCheckbox").uncheck();

cy.get("#editEventForm #interactionCheckbox").uncheck();

cy.get("#editEventForm #joinCheckbox").uncheck();

// Submit the form
cy.get("#editEventForm").submit();

// Wait for the modal to not be visible
cy.get("#editModal").should("not.be.visible");

// Check that all the data is correct
cy.get(".p-name").should("have.text", "Edited Event Name");
cy.get(".p-location").should("have.text", "Edited Event Location");
cy.get(".p-summary").should("contain.text", "Edited Event Description");
cy.get("#hosted-by").should("contain.text", "Hosted by Edited Name");
cy.get(".dt-duration").should(
"contain.text",
"Sunday 1 December 2030 from 12:00 am to 1:00 am",
);
// Check that the comment form is not visible
cy.get("#postComment").should("not.exist");
// Check that the attendee form is not visible
cy.get("#attendEvent").should("not.exist");
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"build": "tsc",
"start": "node dist/start.js",
"dev": "nodemon -e ts,js --watch src --exec \"pnpm run build ; pnpm run start\"",
"test:dev": "pnpm run dev & wait-on http://localhost:3000 && cypress open --e2e --browser chrome",
"test": "pnpm run build || true && pnpm run start & wait-on http://localhost:3000 && cypress run --e2e --browser chrome"
"test:dev": "CYPRESS=true pnpm run dev & wait-on http://localhost:3000 && cypress open --e2e --browser chrome",
"test": "pnpm run build || true && CYPRESS=true pnpm run start & wait-on http://localhost:3000 && cypress run --e2e --browser chrome"
},
"engines": {
"node": ">=16.16.0"
Expand Down
11 changes: 0 additions & 11 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -305,17 +305,6 @@ body, html {
margin-top: 0.5rem;
}

#maxAttendeesContainer {
display: none;
}
/* #maxAttendeesCheckboxContainer {
display: none;
} */

#eventGroupData {
display: none;
}

.edit-buttons {
text-align: right;
}
Expand Down
2 changes: 0 additions & 2 deletions public/js/generate-timezones.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,4 @@ const timezones = [
document.querySelector("#timezone").innerHTML = selectorOptions;

document.querySelector("#timezone").value = moment.tz.guess();

$("#timezone").select2();

12 changes: 11 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import hbs from "express-handlebars";

import routes from "./routes.js";
import frontend from "./routes/frontend.js";
import activitypub from "./routes/activitypub.js";
import event from "./routes/event.js";

import { initEmailService } from "./lib/email.js";

const app = express();

app.locals.sendEmails = initEmailService();

// View engine //
const hbsInstance = hbs.create({
defaultLayout: "main",
Expand Down Expand Up @@ -37,11 +43,15 @@ app.set("hbsInstance", hbsInstance);
app.use(express.static("public"));

// Body parser //
app.use(express.json({ type: "application/activity+json" })); // support json encoded bodies
app.use(express.json({ type: "application/activity+json" }));
app.use(express.json({ type: "application/ld+json" }));
app.use(express.json({ type: "application/json" }));
app.use(express.urlencoded({ extended: true }));

// Router //
app.use("/", frontend);
app.use("/", activitypub);
app.use("/", event);
app.use("/", routes);

export default app;
9 changes: 9 additions & 0 deletions src/lib/activitypub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Request } from "express";

export const acceptsActivityPub = (req: Request) => {
return (
req.headers.accept &&
(req.headers.accept.includes("application/activity+json") ||
req.headers.accept.includes("application/ld+json"))
);
};
5 changes: 3 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "fs";
import toml from "toml";
import { exitWithError } from "./process.js";

interface GathioConfig {
general: {
Expand Down Expand Up @@ -46,8 +47,8 @@ export const getConfig = (): GathioConfig => {
) as GathioConfig;
return config;
} catch {
console.error(
"\x1b[31mConfiguration file not found! Have you renamed './config/config-example.toml' to './config/config.toml'?",
exitWithError(
"Configuration file not found! Have you renamed './config/config-example.toml' to './config/config.toml'?",
);
return process.exit(1);
}
Expand Down
Loading

0 comments on commit b795d07

Please sign in to comment.