Compare commits

...

10 commits

9 changed files with 682 additions and 0 deletions

View file

@ -0,0 +1,99 @@
describe("admin actions", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
cy.seedAdmin({
email: "admin@example.com",
displayName: "rootadmin",
password: "longenoughpassword",
});
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.seedConfirmedUser({
email: "bob@example.com",
displayName: "bob",
password: "longenoughpassword",
});
cy.seedPostAs({
email: "alice@example.com",
password: "longenoughpassword",
title: "Alice post",
body: "Body.",
}).as("alicePost");
});
it("admin promotes another user via the profile page", function () {
cy.loginViaApi({
email: "admin@example.com",
password: "longenoughpassword",
});
cy.visit("/users/alice");
cy.contains("button", "Promote to admin").click();
cy.logoutViaApi();
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Feature in slot 1").should("be.visible");
});
});
it("admin features a post and it shows up on the home page", function () {
cy.loginViaApi({
email: "admin@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Feature in slot 1").click();
cy.contains(".badge", "Featured (slot 1)").should("be.visible");
});
cy.logoutViaApi();
cy.visit("/");
cy.contains("h2", "Featured").should("be.visible");
cy.get(".featured .post-card").should("have.length", 1);
cy.contains(".featured .post-card", "Alice post").should("be.visible");
});
it("admin unfeaturing a post removes it from the home page", function () {
cy.loginViaApi({
email: "admin@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Feature in slot 1").click();
cy.contains(".badge", "Featured (slot 1)").should("be.visible");
cy.contains("button", "Unfeature").click();
cy.contains(".badge", "Featured").should("not.exist");
});
cy.logoutViaApi();
cy.visit("/");
cy.contains("h2", "Featured").should("not.exist");
});
it("a newly promoted admin can also feature posts", function () {
cy.promoteAdmin("bob@example.com");
cy.loginViaApi({
email: "bob@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Feature in slot 2").click();
cy.contains(".badge", "Featured (slot 2)").should("be.visible");
});
});
});

View file

@ -0,0 +1,48 @@
describe("confirm email page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
});
it("redirects to /login when no token is present", function () {
cy.visit("/confirm-email");
cy.location("pathname").should("eq", "/login");
});
it("sets the password and redirects to /login on success", function () {
cy.signupViaApi({
email: "alice@example.com",
displayName: "alice",
});
cy.fetchLatestConfirmToken("alice@example.com").then(function (token) {
cy.visit(`/confirm-email?token=${token}`);
cy.get('input[type="password"]').type("longenoughpassword");
cy.contains("button", "Confirm").click();
cy.location("pathname").should("eq", "/login");
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
});
});
it("shows the backend error when password is too short", function () {
cy.signupViaApi({
email: "alice@example.com",
displayName: "alice",
});
cy.fetchLatestConfirmToken("alice@example.com").then(function (token) {
cy.visit(`/confirm-email?token=${token}`);
cy.get('input[type="password"]')
.invoke("removeAttr", "minlength")
.type("short");
cy.contains("button", "Confirm").click();
cy.contains(".error", "password must be at least 8").should(
"be.visible",
);
});
});
});

View file

@ -0,0 +1,41 @@
describe("guest route guards", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
});
it("redirects anonymous visitors away from /users/:displayName/posts/new", function () {
cy.visit("/users/alice/posts/new");
cy.location("pathname").should("eq", "/login");
});
it("redirects authenticated visitors away from /login", function () {
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/login");
cy.location("pathname").should("eq", "/");
});
it("redirects authenticated visitors away from /signup", function () {
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/signup");
cy.location("pathname").should("eq", "/");
});
});

View file

@ -0,0 +1,101 @@
describe("home page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.seedConfirmedUser({
email: "bob@example.com",
displayName: "bob",
password: "longenoughpassword",
});
cy.seedPostAs({
email: "alice@example.com",
password: "longenoughpassword",
title: "Alice first",
body: "Alice's first post.",
});
cy.seedPostAs({
email: "bob@example.com",
password: "longenoughpassword",
title: "Bob says hi",
body: "Bob's only post.",
});
cy.seedPostAs({
email: "alice@example.com",
password: "longenoughpassword",
title: "Alice second",
body: "Alice's second post.",
});
});
it("renders recent posts in newest-first order with author links", function () {
cy.visit("/");
cy.contains("h2", "Recent posts").should("be.visible");
cy.get(".recent .post-card").should("have.length", 3);
cy.get(".recent .post-card").first().within(function () {
cy.contains("Alice second");
cy.contains("a", "alice");
});
});
it("hides the featured section when no posts are featured", function () {
cy.visit("/");
cy.contains("h2", "Featured").should("not.exist");
});
it("shows featured posts in slot order once an admin features them", function () {
cy.seedAdmin({
email: "admin@example.com",
displayName: "rootadmin",
password: "longenoughpassword",
});
cy.request("/api/posts").then(function (response) {
const posts = response.body.posts as Array<{
id: number;
title: string;
}>;
const aliceSecond = posts.find(function (entry) {
return entry.title === "Alice second";
})!;
const bobPost = posts.find(function (entry) {
return entry.title === "Bob says hi";
})!;
cy.seedFeaturedPost({
adminEmail: "admin@example.com",
adminPassword: "longenoughpassword",
postId: bobPost.id,
slot: 2,
});
cy.seedFeaturedPost({
adminEmail: "admin@example.com",
adminPassword: "longenoughpassword",
postId: aliceSecond.id,
slot: 1,
});
});
cy.visit("/");
cy.contains("h2", "Featured").should("be.visible");
cy.get(".featured .post-card").should("have.length", 2);
cy.get(".featured .post-card").eq(0).contains("Alice second");
cy.get(".featured .post-card").eq(1).contains("Bob says hi");
});
it("searches users by display name and email prefix", function () {
cy.visit("/");
cy.get('input[type="search"]').type("al");
cy.get(".results li").should("have.length", 1);
cy.get(".results li").contains("alice");
cy.get('input[type="search"]').clear().type("b");
cy.get(".results li").should("have.length", 1);
cy.get(".results li").contains("bob");
cy.get('input[type="search"]').clear().type("zzz");
cy.get(".results li").should("have.length", 0);
});
});

View file

@ -0,0 +1,41 @@
describe("login page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
});
it("logs in with valid credentials and shows the user in the header", function () {
cy.visit("/login");
cy.get('input[type="email"]').type("alice@example.com");
cy.get('input[type="password"]').type("longenoughpassword");
cy.contains("button", "Log in").click();
cy.location("pathname").should("eq", "/");
cy.get(".app-header").contains("alice").should("be.visible");
});
it("shows an error on wrong password", function () {
cy.visit("/login");
cy.get('input[type="email"]').type("alice@example.com");
cy.get('input[type="password"]').type("wrongpassword");
cy.contains("button", "Log in").click();
cy.location("pathname").should("eq", "/login");
cy.contains(".error", "invalid credentials").should("be.visible");
});
it("redirects authenticated users away from /login", function () {
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/login");
cy.location("pathname").should("eq", "/");
});
});

View file

@ -0,0 +1,63 @@
describe("new post page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.seedConfirmedUser({
email: "bob@example.com",
displayName: "bob",
password: "longenoughpassword",
});
});
it("redirects anonymous visitors to /login", function () {
cy.visit("/users/alice/posts/new");
cy.location("pathname").should("eq", "/login");
});
it("redirects users away from another user's new-post page", function () {
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/users/bob/posts/new");
cy.location("pathname").should("eq", "/");
});
it("publishes a post and redirects to the new post page", function () {
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/users/alice/posts/new");
cy.get('input[type="text"]').type("Hello world");
cy.get("textarea").type("This is the body of my first post.");
cy.contains("button", "Publish").click();
cy.location("pathname").should("match", /^\/posts\/\d+$/);
cy.contains("h1", "Hello world").should("be.visible");
cy.contains(".body", "This is the body of my first post.").should(
"be.visible",
);
});
it("blocks submission when the title is empty (HTML5 validation)", function () {
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/users/alice/posts/new");
cy.get("textarea").type("body only");
cy.contains("button", "Publish").click();
cy.location("pathname").should("eq", "/users/alice/posts/new");
cy.get('input[type="text"]:invalid').should("exist");
});
});

View file

@ -0,0 +1,136 @@
describe("post page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.seedConfirmedUser({
email: "bob@example.com",
displayName: "bob",
password: "longenoughpassword",
});
cy.seedPostAs({
email: "alice@example.com",
password: "longenoughpassword",
title: "Alice post",
body: "Body of alice's post.",
}).as("alicePost");
});
it("renders the post for anonymous visitors with a login CTA", function () {
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("h1", "Alice post").should("be.visible");
cy.contains("a", "alice").should("be.visible");
cy.contains("Comments (0)").should("be.visible");
cy.contains("Log in").should("be.visible");
cy.get(".comment-form").should("not.exist");
});
});
it("lets a signed-in user add a comment", function () {
cy.loginViaApi({
email: "bob@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.get(".comment-form textarea").type("nice post");
cy.contains(".comment-form button", "Comment").click();
cy.contains(".comment-list li", "nice post").should("be.visible");
cy.contains(".comment-list li", "bob").should("be.visible");
cy.contains("Comments (1)").should("be.visible");
});
});
it("only shows the Delete control on comments you can delete", function () {
cy.loginViaApi({
email: "bob@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.request("POST", `/api/posts/${post.id}/comments`, {
body: "bob's comment",
});
cy.logoutViaApi();
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.request("POST", `/api/posts/${post.id}/comments`, {
body: "alice's comment",
});
cy.visit(`/posts/${post.id}`);
cy.contains(".comment-list li", "alice's comment")
.find("button.delete")
.should("exist");
cy.contains(".comment-list li", "bob's comment")
.find("button.delete")
.should("not.exist");
});
});
it("hides the Delete post control for non-author non-admin users", function () {
cy.loginViaApi({
email: "bob@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Delete post").should("not.exist");
cy.contains("button", "Feature in slot 1").should("not.exist");
});
});
it("lets the author delete their post", function () {
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Delete post").click();
cy.location("pathname").should("eq", "/");
});
});
it("shows admin controls (delete, feature, unfeature) to admins", function () {
cy.seedAdmin({
email: "admin@example.com",
displayName: "rootadmin",
password: "longenoughpassword",
});
cy.loginViaApi({
email: "admin@example.com",
password: "longenoughpassword",
});
cy.get<{ id: number }>("@alicePost").then(function (post) {
cy.visit(`/posts/${post.id}`);
cy.contains("button", "Delete post").should("be.visible");
cy.contains("button", "Feature in slot 1").should("be.visible");
cy.contains("button", "Feature in slot 2").should("be.visible");
cy.contains("button", "Feature in slot 1").click();
cy.contains(".badge", "Featured (slot 1)").should("be.visible");
cy.contains("button", "Unfeature").should("be.visible");
cy.contains("button", "Unfeature").click();
cy.contains(".badge", "Featured").should("not.exist");
});
});
it("shows a not-found panel for unknown post ids", function () {
cy.visit("/posts/9999");
cy.contains("h1", "Post not found").should("be.visible");
});
});

View file

@ -0,0 +1,86 @@
describe("profile page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.seedConfirmedUser({
email: "bob@example.com",
displayName: "bob",
password: "longenoughpassword",
});
cy.seedPostAs({
email: "alice@example.com",
password: "longenoughpassword",
title: "Alice's only post",
body: "Hello.",
});
});
it("shows posts to anonymous visitors without owner controls", function () {
cy.visit("/users/alice");
cy.contains("h1", "alice").should("be.visible");
cy.contains(".post-card", "Alice's only post").should("be.visible");
cy.contains("button", "New post").should("not.exist");
cy.contains("button", "Promote to admin").should("not.exist");
});
it("shows the New post button when viewing your own profile", function () {
cy.loginViaApi({
email: "alice@example.com",
password: "longenoughpassword",
});
cy.visit("/users/alice");
cy.contains("button", "New post").should("be.visible");
cy.contains("button", "Promote to admin").should("not.exist");
});
it("hides the Promote button from non-admin viewers", function () {
cy.loginViaApi({
email: "bob@example.com",
password: "longenoughpassword",
});
cy.visit("/users/alice");
cy.contains("button", "Promote to admin").should("not.exist");
});
it("shows the Promote button to an admin viewing someone else", function () {
cy.seedAdmin({
email: "admin@example.com",
displayName: "rootadmin",
password: "longenoughpassword",
});
cy.loginViaApi({
email: "admin@example.com",
password: "longenoughpassword",
});
cy.visit("/users/alice");
cy.contains("button", "Promote to admin").should("be.visible");
});
it("hides the Promote button when an admin views their own profile", function () {
cy.seedAdmin({
email: "admin@example.com",
displayName: "rootadmin",
password: "longenoughpassword",
});
cy.loginViaApi({
email: "admin@example.com",
password: "longenoughpassword",
});
cy.visit("/users/rootadmin");
cy.contains("button", "Promote to admin").should("not.exist");
});
it("renders a not-found panel for unknown display names", function () {
cy.visit("/users/nobody");
cy.contains("h1", "User not found").should("be.visible");
});
});

View file

@ -0,0 +1,67 @@
describe("signup page", function () {
beforeEach(function () {
cy.resetDb();
cy.clearMail();
});
it("creates an unconfirmed user and redirects to check-email", function () {
cy.visit("/signup");
cy.get('input[type="email"]').type("alice@example.com");
cy.get('input[autocomplete="username"]').type("alice");
cy.contains("button", "Continue").click();
cy.location("pathname").should("eq", "/check-email");
cy.contains("h1", "Check your email").should("be.visible");
cy.getMail().then(function (inbox) {
expect(inbox.messages).to.have.length(1);
expect(inbox.messages[0].To[0].Address).to.equal(
"alice@example.com",
);
});
});
it("surfaces a 409 when the email is already registered", function () {
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.clearMail();
cy.visit("/signup");
cy.get('input[type="email"]').type("alice@example.com");
cy.get('input[autocomplete="username"]').type("alice2");
cy.contains("button", "Continue").click();
cy.location("pathname").should("eq", "/signup");
cy.contains(".error", "email already registered").should("be.visible");
});
it("surfaces a 409 when the display name is taken", function () {
cy.seedConfirmedUser({
email: "alice@example.com",
displayName: "alice",
password: "longenoughpassword",
});
cy.clearMail();
cy.visit("/signup");
cy.get('input[type="email"]').type("other@example.com");
cy.get('input[autocomplete="username"]').type("alice");
cy.contains("button", "Continue").click();
cy.location("pathname").should("eq", "/signup");
cy.contains(".error", "displayName already taken").should("be.visible");
});
it("blocks invalid display name characters client-side", function () {
cy.visit("/signup");
cy.get('input[type="email"]').type("alice@example.com");
cy.get('input[autocomplete="username"]').type("Has Spaces");
cy.contains("button", "Continue").click();
cy.location("pathname").should("eq", "/signup");
cy.get('input[autocomplete="username"]:invalid').should("exist");
});
});