Skip to content
Open
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
4 changes: 4 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ def send_bloom():
return type_check_error

user = get_current_user()
# Check server-side length
content=request.json["content"]
if len(content)>280 :
return jsonify({"success": False, "error": "Bloom must be 280 characters or less"}), 400

blooms.add_bloom(sender=user, content=request.json["content"])

Expand Down
11 changes: 8 additions & 3 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ const createBloom = (template, bloom) => {

function _formatHashtags(text) {
if (!text) return text;
return text.replace(
/\B#[^#]+/g,
(match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`

// Normalize newlines and tabs to spaces
const normalizedText = text.replace(/[\r\n\t]+/g, " ");

return normalizedText.replace(
// Updated regex to correctly handle multiple hashtags with letters, numbers, and underscores
/#([a-zA-Z0-9_]+)/g,
(match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`,
);
}

Expand Down
11 changes: 9 additions & 2 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {state} from "../index.mjs";
import {handleErrorDialog} from "../components/error.mjs";


// === ABOUT THE STATE
// state gives you these two functions only
// updateState({stateKey: newValues})
Expand Down Expand Up @@ -194,10 +195,16 @@ async function getBloomsByHashtag(hashtag) {
}

async function postBloom(content) {
// Check client-side length first
if (content.length>280){
handleErrorDialog(new Error("Bloom must be 280 characters or less"));
return { success: false };

}
try {
const data = await _apiRequest("/bloom", {
method: "POST",
body: JSON.stringify({content}),
body: JSON.stringify({ content }),
});

if (data.success) {
Expand All @@ -208,7 +215,7 @@ async function postBloom(content) {
return data;
} catch (error) {
// Error already handled by _apiRequest
return {success: false};
return { success: false };
}
}

Expand Down
34 changes: 34 additions & 0 deletions front-end/tests/bloom-length.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { test, expect } from "@playwright/test";
import { loginAsSample } from "./test-utils";

test("server should reject blooms longer than 280 characters", async ({
page,
}) => {

await loginAsSample(page);

const longBloom = "A".repeat(281);

const result = await page.evaluate(async (content) => {
const res = await fetch("/bloom", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ content }),
});

if (!res.ok) {
try {
return await res.json();
} catch {
return { success: false };
}
}

return await res.json();
}, longBloom);

expect(result.success).toBe(false);
});
24 changes: 24 additions & 0 deletions front-end/tests/hashtag.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from "@playwright/test";

test("should not make infinite hashtag endpoint requests", async ({ page }) => {
// ===== ARRANGE
const requests = [];
page.on("request", (request) => {
if (
request.url().includes(":3000/hashtag/do") &&
request.resourceType() === "fetch"
) {
requests.push(request);
}
});
// ====== ACT
// When I navigate to the hashtag
await page.goto("/#/hashtag/do");
// And I wait a reasonable time for any additional requests
await page.waitForTimeout(200);

// ====== ASSERT
// Then the number of requests should be 1
console.log("Number of requests:", requests.length);
expect(requests.length).toEqual(1);
});
28 changes: 28 additions & 0 deletions front-end/tests/profile-logout-login.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { test, expect } from "@playwright/test";
import { loginAsSample, logout } from "./test-utils.mjs";

test("can visit another user's profile after logout and re-login", async ({
page,
}) => {
// Given I am logged in
await loginAsSample(page);

// And I visit another user's profile
await page.goto("/#/profile/AS");

// And I log out
await logout(page);

// When I log in again
await loginAsSample(page);

// And I visit the same profile again
await page.goto("/#/profile/AS");

// Then I should see the profile view (NOT a server error)
await expect(page.locator("#profile-container")).toBeVisible();

await expect(page.locator("body")).not.toContainText(
"Server does not support this operation",
);
});
21 changes: 14 additions & 7 deletions front-end/views/hashtag.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,45 @@ import {createHeading} from "../components/heading.mjs";
function hashtagView(hashtag) {
destroy();

apiService.getBloomsByHashtag(hashtag);
// Normalize the hashtag to always include a leading '#' and
// prevent infinite API calls by only fetching if the hashtag has changed

const normalizedHashtag = hashtag.startsWith("#") ? hashtag : `#${hashtag}`;
if (state.currentHashtag !== normalizedHashtag) {
state.currentHashtag = normalizedHashtag;
apiService.getBloomsByHashtag(normalizedHashtag);
}
renderOne(
state.isLoggedIn,
getLogoutContainer(),
"logout-template",
createLogout
createLogout,
);

document
.querySelector("[data-action='logout']")
?.addEventListener("click", handleLogout);
renderOne(
state.isLoggedIn,
getLoginContainer(),
"login-template",
createLogin
createLogin,
);
document
.querySelector("[data-action='login']")
?.addEventListener("click", handleLogin);
.querySelector("[data-form='login']")
?.addEventListener("submit", handleLogin);

renderOne(
state.currentHashtag,
getHeadingContainer(),
"heading-template",
createHeading
createHeading,
);
renderEach(
state.hashtagBlooms || [],
getTimelineContainer(),
"bloom-template",
createBloom
createBloom,
);
}

Expand Down
4 changes: 2 additions & 2 deletions front-end/views/profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ function profileView(username) {
createLogin
);
document
.querySelector("[data-action='login']")
?.addEventListener("click", handleLogin);
.querySelector("[data-form='login']")
?.addEventListener("submit", handleLogin);

const profileData = state.profiles.find((p) => p.username === username);
if (profileData) {
Expand Down