Create Todo app
Summary: Ref T2687 Reviewers: #testers, kuba-orlik Reviewed By: #testers, kuba-orlik Subscribers: kuba-orlik, jenkins-user Maniphest Tasks: T2687 Differential Revision: https://hub.sealcode.org/D1339master
parent
b7cc271d97
commit
0c49effa9d
@ -0,0 +1,9 @@
|
|||||||
|
import { LONG_TEST_TIMEOUT } from "./src/back/test_utils/webhint";
|
||||||
|
import { closeBrowser } from "./src/back/test_utils/browser-creator";
|
||||||
|
|
||||||
|
exports.mochaHooks = {
|
||||||
|
async afterAll() {
|
||||||
|
this.timeout(LONG_TEST_TIMEOUT);
|
||||||
|
await closeBrowser();
|
||||||
|
},
|
||||||
|
};
|
@ -1,12 +1,14 @@
|
|||||||
#!/usr/bin/env -S bash -x
|
#!/usr/bin/env -S bash -x
|
||||||
|
|
||||||
|
|
||||||
# the "--no-TTY" option is crucial - without it the output is not captured in Jenkins
|
# the "--no-TTY" option is crucial - without it the output is not captured in Jenkins
|
||||||
|
|
||||||
docker-compose run \
|
CONTAINER_ID=$(docker-compose run \
|
||||||
--rm \
|
-d \
|
||||||
--service-ports \
|
--service-ports \
|
||||||
-e "SEALIOUS_MONGO_PORT=27017" \
|
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||||
-e "SEALIOUS_MONGO_HOST=db" \
|
-e "SEALIOUS_MONGO_HOST=db" \
|
||||||
test \
|
test \
|
||||||
npm "$@"
|
npm "$@")
|
||||||
|
|
||||||
|
docker logs -f $CONTAINER_ID
|
||||||
|
docker rm $CONTAINER_ID
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
|
||||||
|
export default class Tasks extends Collection {
|
||||||
|
fields = {
|
||||||
|
title: new FieldTypes.Text(),
|
||||||
|
done: new (class extends FieldTypes.Boolean {
|
||||||
|
hasDefaultValue = () => true;
|
||||||
|
async getDefaultValue() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
|
||||||
|
policies = {
|
||||||
|
create: new Policies.Public(),
|
||||||
|
show: new Policies.Owner(),
|
||||||
|
list: new Policies.Owner(),
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultPolicy = new Policies.Public();
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
const ADMIN_CREDENTIALS = {
|
||||||
|
username: "admin",
|
||||||
|
password: "adminadmin",
|
||||||
|
email: "admin@example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ADMIN_CREDENTIALS;
|
@ -0,0 +1,53 @@
|
|||||||
|
import { BaseContext } from "koa";
|
||||||
|
import { CollectionItem } from "sealious";
|
||||||
|
import frame from "../../frame";
|
||||||
|
import { Tasks } from "../../collections/collections";
|
||||||
|
|
||||||
|
export function Task(task: CollectionItem<typeof Tasks>) {
|
||||||
|
return frame(
|
||||||
|
`task-${task.id}`,
|
||||||
|
/* HTML */ `<li class="task">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-controller="task"
|
||||||
|
data-action="task#toggle"
|
||||||
|
data-id="${task.id}"
|
||||||
|
${task.get("done") ? "checked" : ""}
|
||||||
|
/>
|
||||||
|
${task.get("title") as string}
|
||||||
|
<form method="POST" action="/todo/">
|
||||||
|
<input class="delete-button" type="submit" value="Delete" />
|
||||||
|
<input
|
||||||
|
class="hidden-button"
|
||||||
|
type="hidden"
|
||||||
|
name="taskId"
|
||||||
|
value="${task.id}"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="hidden-button"
|
||||||
|
type="hidden"
|
||||||
|
id="action"
|
||||||
|
name="action"
|
||||||
|
value="delete"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</li>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
<form method="DELETE" action="/todo/${task.id}">
|
||||||
|
<input class="delete-button" type="submit" value="Delete" />
|
||||||
|
</form>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function TaskList(ctx: BaseContext) {
|
||||||
|
const { items: tasks } = await ctx.$app.collections.tasks.list(ctx.$context).fetch();
|
||||||
|
|
||||||
|
const tasksTemplate = tasks.map(Task).join("\n");
|
||||||
|
return `
|
||||||
|
<ul>
|
||||||
|
${tasksTemplate}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import { Mountable } from "@sealcode/sealgen";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
|
||||||
|
export const actionName = "Logout";
|
||||||
|
|
||||||
|
export default new (class LogoutRedirect extends Mountable {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(_: Context) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(router: Router, path: string) {
|
||||||
|
router.get(path, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const session_id: string = ctx.cookies.get("sealious-session") as string;
|
||||||
|
if (session_id) {
|
||||||
|
await ctx.$app.collections.sessions.logout(
|
||||||
|
new ctx.$app.SuperContext(),
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
ctx.status = 302;
|
||||||
|
ctx.redirect("/");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during logout:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,55 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
import { LONG_TEST_TIMEOUT, VERY_LONG_TEST_TIMEOUT } from "../test_utils/webhint";
|
||||||
|
import { LogoutURL, SignInURL } from "./urls";
|
||||||
|
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
import { getBrowser } from "../test_utils/browser-creator";
|
||||||
|
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||||
|
|
||||||
|
describe("Logout", () => {
|
||||||
|
let page: Page;
|
||||||
|
let browser: Browser;
|
||||||
|
let context: BrowserContext;
|
||||||
|
const username = ADMIN_CREDENTIALS.username;
|
||||||
|
const password = ADMIN_CREDENTIALS.password;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't crash", async function () {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
return withProdApp(async ({ rest_api }) => {
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await rest_api.get(LogoutURL);
|
||||||
|
},
|
||||||
|
{ name: "Error" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logout test", () => {
|
||||||
|
it("logout", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill(password);
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||||
|
await page.getByRole("link", { name: "Logout" }).click();
|
||||||
|
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,121 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormData,
|
||||||
|
FormDataValue,
|
||||||
|
Fields,
|
||||||
|
Controls,
|
||||||
|
FormReaction,
|
||||||
|
} from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
import { Users } from "../collections/collections";
|
||||||
|
import { FlatTemplatable, tempstream } from "tempstream";
|
||||||
|
import { PageErrorMessage } from "@sealcode/sealgen/@types/page/mountable-with-fields";
|
||||||
|
|
||||||
|
export const actionName = "SignIn";
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
username: new Fields.SimpleFormField(true),
|
||||||
|
password: new Fields.SimpleFormField(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignInShape = Fields.fieldsToShape(fields);
|
||||||
|
|
||||||
|
export default new (class SignInForm extends Form<typeof fields, void> {
|
||||||
|
defaultSuccessMessage = "Formularz wypełniony poprawnie";
|
||||||
|
fields = fields;
|
||||||
|
|
||||||
|
controls = [
|
||||||
|
new Controls.SimpleInput(fields.username, { label: "Username:", type: "text" }),
|
||||||
|
new Controls.SimpleInput(fields.password, {
|
||||||
|
label: "Password:",
|
||||||
|
type: "password",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
async validateValues(
|
||||||
|
ctx: Context,
|
||||||
|
data: Record<string, FormDataValue>
|
||||||
|
): Promise<{ valid: boolean; error: string }> {
|
||||||
|
const { parsed: username } = await this.fields.username.getValue(ctx, data);
|
||||||
|
|
||||||
|
const filter: object = typeof username === "string" ? { username } : {};
|
||||||
|
|
||||||
|
const user = await Users.suList().filter(filter).fetch();
|
||||||
|
|
||||||
|
if (user.empty) {
|
||||||
|
return { valid: false, error: `Incorrect password or username` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, error: `` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(ctx: Context) {
|
||||||
|
if (ctx.$context.session_id) {
|
||||||
|
return { canAccess: false, message: "" };
|
||||||
|
}
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSuccess(
|
||||||
|
_: Context,
|
||||||
|
__: FormData<string>,
|
||||||
|
_submitResult: void
|
||||||
|
): Promise<FormReaction> {
|
||||||
|
const reaction: FormReaction = {
|
||||||
|
action: "redirect",
|
||||||
|
url: "/",
|
||||||
|
};
|
||||||
|
console.log("Successfully logged in.");
|
||||||
|
return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onError(
|
||||||
|
ctx: Context,
|
||||||
|
data: FormData<string>,
|
||||||
|
error: unknown
|
||||||
|
): Promise<FormReaction> {
|
||||||
|
const reaction: FormReaction = {
|
||||||
|
action: "stay",
|
||||||
|
content: this.render(ctx, data, true),
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
text: `There was an error while logging in: ${String(error)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubmit(ctx: Context, data: FormData) {
|
||||||
|
try {
|
||||||
|
const sessionId: string = await Users.app.collections.sessions.login(
|
||||||
|
data.raw_values.username as string,
|
||||||
|
data.raw_values.password as string
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.cookies.set("sealious-session", sessionId, {
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7,
|
||||||
|
secure: ctx.request.protocol === "https",
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(String(error));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderError(ctx: Context, error: PageErrorMessage): Promise<FlatTemplatable> {
|
||||||
|
return html(ctx, "SignIn", `${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||||
|
return html(
|
||||||
|
ctx,
|
||||||
|
"SignIn",
|
||||||
|
tempstream`${await super.render(ctx, data, show_field_errors)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,103 @@
|
|||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||||
|
import { SignInURL, LogoutURL } from "./urls";
|
||||||
|
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
import { getBrowser } from "../test_utils/browser-creator";
|
||||||
|
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||||
|
|
||||||
|
describe("SignIn", () => {
|
||||||
|
let page: Page;
|
||||||
|
let browser: Browser;
|
||||||
|
let context: BrowserContext;
|
||||||
|
const username = ADMIN_CREDENTIALS.username;
|
||||||
|
const password = ADMIN_CREDENTIALS.password;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't crash", async function () {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
|
await rest_api.get(SignInURL);
|
||||||
|
await webhintURL(base_url + SignInURL);
|
||||||
|
// alternatively you can use webhintHTML for faster but less precise scans
|
||||||
|
// or for scanning responses of requests that use some form of authorization:
|
||||||
|
// const response = await rest_api.get(SignInURL);
|
||||||
|
// await webhintHTML(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("can access test", () => {
|
||||||
|
it("access url", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill(password);
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||||
|
await page.goto(base_url + SignInURL);
|
||||||
|
await page.waitForSelector('body:has-text("no access")');
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Logout" }).click();
|
||||||
|
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sign in test", () => {
|
||||||
|
it("wrong username", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill("username20230720722");
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill("test");
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.waitForSelector(".form-message");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct username and password", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill(password);
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||||
|
await page.getByRole("link", { name: "Logout" }).click();
|
||||||
|
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wrong password", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill("asddasads20230720722");
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.waitForSelector(".form-message");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,139 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormData,
|
||||||
|
FormDataValue,
|
||||||
|
Fields,
|
||||||
|
Controls,
|
||||||
|
FormReaction,
|
||||||
|
} from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
import { Users } from "../collections/collections";
|
||||||
|
|
||||||
|
export const actionName = "SignUp";
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
username: new Fields.CollectionField(true, Users.fields.username),
|
||||||
|
email: new Fields.CollectionField(true, Users.fields.email),
|
||||||
|
password: new Fields.SimpleFormField(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignUpShape = Fields.fieldsToShape(fields);
|
||||||
|
|
||||||
|
export default new (class SignUpForm extends Form<typeof fields, void> {
|
||||||
|
defaultSuccessMessage = "Formularz wypełniony poprawnie";
|
||||||
|
fields = fields;
|
||||||
|
|
||||||
|
controls = [
|
||||||
|
new Controls.SimpleInput(fields.username, { label: "Username:", type: "text" }),
|
||||||
|
new Controls.SimpleInput(fields.email, { label: "Email:", type: "email" }),
|
||||||
|
new Controls.SimpleInput(fields.password, {
|
||||||
|
label: "Password:",
|
||||||
|
type: "password",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
async validateValues(
|
||||||
|
ctx: Context,
|
||||||
|
data: Record<string, FormDataValue>
|
||||||
|
): Promise<{ valid: boolean; error: string }> {
|
||||||
|
const { parsed: email } = await this.fields.email.getValue(ctx, data);
|
||||||
|
const { parsed: password } = await this.fields.password.getValue(ctx, data);
|
||||||
|
|
||||||
|
if ((password || "").length >= 8) {
|
||||||
|
const user = await Users.suList().filter({ email: email }).fetch();
|
||||||
|
if (user.empty) {
|
||||||
|
return { valid: true, error: `` };
|
||||||
|
}
|
||||||
|
return { valid: false, error: `Email is arleady taken` };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Password must contain a minimum of 8 characters",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(ctx: Context) {
|
||||||
|
if (ctx.$context.session_id) {
|
||||||
|
return { canAccess: false, message: "" };
|
||||||
|
}
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async onError(
|
||||||
|
ctx: Context,
|
||||||
|
data: FormData<string>,
|
||||||
|
error: unknown
|
||||||
|
): Promise<FormReaction> {
|
||||||
|
const reaction: FormReaction = {
|
||||||
|
action: "stay",
|
||||||
|
content: this.render(ctx, data, true),
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
text: `An unexpected error occurred, try again. <br> Error${
|
||||||
|
error as string
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSuccess(ctx: Context, data: FormData): Promise<FormReaction> {
|
||||||
|
const username: FormDataValue = data.raw_values.username;
|
||||||
|
const reaction: FormReaction = {
|
||||||
|
action: "stay",
|
||||||
|
content: `Hello ${String(
|
||||||
|
username
|
||||||
|
)}. <p class="success-notify">Your account has been successfully created.</p>
|
||||||
|
<a href="/" class="nav-logo">
|
||||||
|
<img
|
||||||
|
src="/assets/logo"
|
||||||
|
alt="${ctx.$app.manifest.name} - logo"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
/>
|
||||||
|
Sealious App
|
||||||
|
</a>`,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "success",
|
||||||
|
text: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubmit(ctx: Context, data: FormData) {
|
||||||
|
const username: string =
|
||||||
|
typeof data.raw_values.username === "string" ? data.raw_values.username : "";
|
||||||
|
const password: string =
|
||||||
|
typeof data.raw_values.password === "string" ? data.raw_values.password : "";
|
||||||
|
const email: string =
|
||||||
|
typeof data.raw_values.email === "string" ? data.raw_values.email : "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Users.suCreate({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
email: email,
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
console.log("A user was created successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during user creation:", error);
|
||||||
|
throw new Error(String(error));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||||
|
return html(ctx, "SignUp", await super.render(ctx, data, show_field_errors));
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,122 @@
|
|||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||||
|
import { SignUpURL, LogoutURL, SignInURL } from "./urls";
|
||||||
|
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
import { getBrowser } from "../test_utils/browser-creator";
|
||||||
|
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||||
|
|
||||||
|
describe("SignUp", () => {
|
||||||
|
let page: Page;
|
||||||
|
let browser: Browser;
|
||||||
|
let context: BrowserContext;
|
||||||
|
const username = ADMIN_CREDENTIALS.username;
|
||||||
|
const password = ADMIN_CREDENTIALS.password;
|
||||||
|
const email = ADMIN_CREDENTIALS.email;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't crash", async function () {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
|
await rest_api.get(SignUpURL);
|
||||||
|
await webhintURL(base_url + SignUpURL);
|
||||||
|
// alternatively you can use webhintHTML for faster but less precise scans
|
||||||
|
// or for scanning responses of requests that use some form of authorization:
|
||||||
|
// const response = await rest_api.get(SignUpURL);
|
||||||
|
// await webhintHTML(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("signup test", () => {
|
||||||
|
it("username is taken", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign up" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("email").fill("user0192939@randomsuper.com");
|
||||||
|
await page.getByPlaceholder("email").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill("user12341234");
|
||||||
|
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||||
|
await page.waitForSelector(".input__error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("password is too shot ", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign up" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill("dasdsa");
|
||||||
|
await page.getByPlaceholder("email").click();
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("email")
|
||||||
|
.fill("asasdsdadsadss123asddsa@asdasca.com");
|
||||||
|
await page.getByPlaceholder("password").click();
|
||||||
|
await page.getByPlaceholder("password").fill("asddsa");
|
||||||
|
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||||
|
await page.waitForSelector(".form-message.form-message--error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("email is taken", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign up" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill("ranomusername2023072722");
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("email").fill(email);
|
||||||
|
await page.getByPlaceholder("email").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill("asdasdasdasdasd");
|
||||||
|
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||||
|
await page.waitForSelector(".form-message.form-message--error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("correct", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign up" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill("ranomusername20230720722");
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("email").fill("radomemail@emailrandom.com");
|
||||||
|
await page.getByPlaceholder("email").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill("asdasdasdasdasd");
|
||||||
|
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||||
|
await page.waitForSelector(".success-notify");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("can access test", () => {
|
||||||
|
it("access url", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill(password);
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||||
|
await page.goto(base_url + SignUpURL);
|
||||||
|
await page.waitForSelector('body:has-text("no access")');
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Logout" }).click();
|
||||||
|
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,107 @@
|
|||||||
|
import { Tasks } from "./../collections/collections";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { Context } from "koa";
|
||||||
|
import { Form, FormData, FormDataValue, Fields, Controls } from "@sealcode/sealgen";
|
||||||
|
import html from "../html";
|
||||||
|
import { TaskList } from "./common/tasks-view";
|
||||||
|
|
||||||
|
export const actionName = "Todo";
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
name: new Fields.CollectionField(true, Tasks.fields.title),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TodoShape = Fields.fieldsToShape(fields);
|
||||||
|
|
||||||
|
export default new (class TodoForm extends Form<typeof fields, void> {
|
||||||
|
defaultSuccessMessage = "Task has been successfully created";
|
||||||
|
fields = fields;
|
||||||
|
|
||||||
|
controls = [
|
||||||
|
new Controls.SimpleInput(fields.name, {
|
||||||
|
label: "Task name:",
|
||||||
|
type: "text",
|
||||||
|
placeholder: "Write an Matrix bot",
|
||||||
|
}),
|
||||||
|
new Controls.HTML("decoration", (fctx) => {
|
||||||
|
return `<input class="hidden-button" type="hidden" id="action" name="action" value="create" form="${fctx.form_id}" />`;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
async validateValues(
|
||||||
|
ctx: Context,
|
||||||
|
data: Record<string, FormDataValue>
|
||||||
|
): Promise<{ valid: boolean; error: string }> {
|
||||||
|
const { parsed: name } = await this.fields.name.getValue(ctx, data);
|
||||||
|
|
||||||
|
if ((name || "").length < 3) {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
error: "The name of the task must have at least 3 characters",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const filter: object = name ? { title: name } : {};
|
||||||
|
|
||||||
|
const tasks = await ctx.$app.collections.tasks
|
||||||
|
.list(ctx.$context)
|
||||||
|
.filter(filter)
|
||||||
|
.fetch();
|
||||||
|
if (tasks.empty) {
|
||||||
|
return { valid: true, error: "" };
|
||||||
|
}
|
||||||
|
return { valid: false, error: "Task with the same name already exists" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async canAccess(ctx: Context) {
|
||||||
|
if (ctx.$context.session_id) {
|
||||||
|
return { canAccess: true, message: "" };
|
||||||
|
}
|
||||||
|
return { canAccess: false, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubmit(ctx: Context, data: FormData) {
|
||||||
|
const action: FormDataValue = data.raw_values.action;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create": {
|
||||||
|
try {
|
||||||
|
await ctx.$app.collections.tasks.create(ctx.$context, {
|
||||||
|
title: String(data.raw_values.name),
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
console.debug(`task has been successfully created`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete": {
|
||||||
|
const task = await ctx.$app.collections.tasks.getByID(
|
||||||
|
ctx.$context,
|
||||||
|
data.raw_values.taskId as string
|
||||||
|
);
|
||||||
|
await task.remove(ctx.$context);
|
||||||
|
console.debug(`task has been successfully removed`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.debug("Wrong action");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||||
|
return html(
|
||||||
|
ctx,
|
||||||
|
"Todo",
|
||||||
|
tempstream`${await super.render(ctx, data, show_field_errors)}
|
||||||
|
${TaskList(ctx)}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,81 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
import { LONG_TEST_TIMEOUT, VERY_LONG_TEST_TIMEOUT } from "../test_utils/webhint";
|
||||||
|
import { SignInURL, TodoURL } from "./urls";
|
||||||
|
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
import { getBrowser } from "../test_utils/browser-creator";
|
||||||
|
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||||
|
|
||||||
|
describe("Todo", function () {
|
||||||
|
let page: Page;
|
||||||
|
let browser: Browser;
|
||||||
|
let context: BrowserContext;
|
||||||
|
const username = ADMIN_CREDENTIALS.username;
|
||||||
|
const password = ADMIN_CREDENTIALS.password;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't crash", async function () {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
return withProdApp(async ({ rest_api }) => {
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await rest_api.get(TodoURL);
|
||||||
|
},
|
||||||
|
{ name: "Error" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("todo test", () => {
|
||||||
|
it("create and delete task", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await page.getByPlaceholder("text").click();
|
||||||
|
await page.getByPlaceholder("text").fill(username);
|
||||||
|
await page.getByPlaceholder("text").press("Tab");
|
||||||
|
await page.getByPlaceholder("password").fill(password);
|
||||||
|
await page.getByPlaceholder("password").press("Enter");
|
||||||
|
await page.getByRole("link", { name: "To do app" }).click();
|
||||||
|
await page.getByPlaceholder("Write an Matrix bot").click();
|
||||||
|
await page.getByPlaceholder("Write an Matrix bot").fill("randomtasdk");
|
||||||
|
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||||
|
await page.waitForSelector(".form-message.form-message--success");
|
||||||
|
await page.locator("turbo-frame").getByRole("checkbox").check();
|
||||||
|
await page.locator("turbo-frame").getByRole("checkbox").uncheck();
|
||||||
|
await page
|
||||||
|
.locator("turbo-frame")
|
||||||
|
.getByRole("button", { name: "Delete" })
|
||||||
|
.click();
|
||||||
|
await page.getByRole("link", { name: "Logout" }).click();
|
||||||
|
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("can access test", () => {
|
||||||
|
it("access url", async function () {
|
||||||
|
await withProdApp(async ({ base_url }) => {
|
||||||
|
this.timeout(LONG_TEST_TIMEOUT);
|
||||||
|
await page.goto(base_url);
|
||||||
|
try {
|
||||||
|
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||||
|
await page.goto(base_url + TodoURL);
|
||||||
|
await page.waitForSelector('body:has-text("no access")');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1 +1,5 @@
|
|||||||
export const HelloURL = "/hello/";
|
export const HelloURL = "/hello/";
|
||||||
|
export const LogoutURL = "/logout/";
|
||||||
|
export const SignInURL = "/signIn/";
|
||||||
|
export const SignUpURL = "/signUp/";
|
||||||
|
export const TodoURL = "/todo/";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
describe("sample test", () => {
|
describe("sample test", () => {
|
||||||
it("always passes", () => {
|
it("always passes", () => {
|
||||||
return true;
|
return;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { Browser, firefox } from "@playwright/test";
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export async function getBrowser(): Promise<Browser> {
|
||||||
|
if (!browser) browser = await firefox.launch();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeBrowser() {
|
||||||
|
if (browser) {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
export default class TaskController extends Controller {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const dataIdAttr = this.element.getAttribute("data-id");
|
||||||
|
if (dataIdAttr) {
|
||||||
|
this.id = dataIdAttr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggle(event: Event) {
|
||||||
|
const inputElement: HTMLInputElement = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (inputElement instanceof HTMLInputElement) {
|
||||||
|
const isChecked: boolean = inputElement.checked;
|
||||||
|
|
||||||
|
await fetch(`/api/v1/collections/tasks/${this.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
done: isChecked,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import * as Turbo from "@hotwired/turbo";
|
import * as Turbo from "@hotwired/turbo";
|
||||||
import { Application } from "stimulus";
|
import { Application } from "stimulus";
|
||||||
|
import TaskController from "./controllers/task-controller";
|
||||||
|
|
||||||
export { Turbo };
|
export { Turbo };
|
||||||
|
|
||||||
const application = Application.start();
|
const application = Application.start();
|
||||||
|
application.register("task", TaskController);
|
||||||
|
Loading…
Reference in New Issue