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/D1339
This commit is contained in:
parent
b7cc271d97
commit
0c49effa9d
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,6 +6,7 @@ tmp/
|
|||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
node_modules
|
node_modules
|
||||||
|
docker_node_modules
|
||||||
db
|
db
|
||||||
coverage.html
|
coverage.html
|
||||||
\#*
|
\#*
|
||||||
@ -27,3 +28,5 @@ public/dist
|
|||||||
/hint-report/
|
/hint-report/
|
||||||
.vscode
|
.vscode
|
||||||
.env
|
.env
|
||||||
|
.npm_cache/
|
||||||
|
.mozilla/
|
||||||
|
@ -11,8 +11,9 @@ services:
|
|||||||
dockerfile: ./test.Dockerfile
|
dockerfile: ./test.Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/opt/sealious-app/
|
- ./:/opt/sealious-app/
|
||||||
- ~/.npm_cacache:/opt/sealious-app/.npm_cacache
|
- /tmp:/tmp
|
||||||
user: ${UID:-1000}:${GID:-1000}
|
- ./docker_node_modules:/opt/sealious-app/node_modules
|
||||||
|
user: ${USER_ID:-1000}:${GROUP_ID:-1000}
|
||||||
mailcatcher:
|
mailcatcher:
|
||||||
image: schickling/mailcatcher:latest
|
image: schickling/mailcatcher:latest
|
||||||
ports:
|
ports:
|
||||||
|
@ -10,13 +10,16 @@ RUN apt update
|
|||||||
RUN apt install -y git
|
RUN apt install -y git
|
||||||
RUN apt install -y tmux
|
RUN apt install -y tmux
|
||||||
|
|
||||||
|
# playwright deps
|
||||||
|
RUN apt-get update&& apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xvfb fonts-noto-color-emoji fonts-unifont libfontconfig1 libfreetype6 xfonts-cyrillic xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf libcairo-gobject2 libdbus-glib-1-2 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu67 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libsoup2.4-1 libwayland-egl1 libwayland-server0 libwebp6 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7
|
||||||
|
|
||||||
RUN chmod +x /tini
|
RUN chmod +x /tini
|
||||||
ENTRYPOINT ["/tini", "--"]
|
ENTRYPOINT ["/tini", "--"]
|
||||||
|
|
||||||
VOLUME $HOME
|
VOLUME $HOME
|
||||||
WORKDIR $HOME
|
WORKDIR $HOME
|
||||||
|
|
||||||
RUN npm install -g npm@latest
|
RUN npm install -g npm@7
|
||||||
RUN npm install -g @sealcode/sealgen
|
RUN npm install -g @sealcode/sealgen
|
||||||
|
|
||||||
USER $UID:$GID
|
USER $UID:$GID
|
||||||
|
@ -3,6 +3,7 @@ set -e
|
|||||||
|
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
|
||||||
|
npm install @sealcode/sealgen
|
||||||
npx sealgen make-env
|
npx sealgen make-env
|
||||||
cp secrets.example.json secrets.json
|
cp secrets.example.json secrets.json
|
||||||
|
|
||||||
@ -11,17 +12,13 @@ SEALIOUS_BASE_URL=$(cat .base_url)
|
|||||||
export SEALIOUS_BASE_URL
|
export SEALIOUS_BASE_URL
|
||||||
echo "PORT=$PORT" >> .env
|
echo "PORT=$PORT" >> .env
|
||||||
|
|
||||||
# Create the npm cache directory if it isn't present yet. If it is not present, it will be created
|
mkdir -p node_modules
|
||||||
# when the docker image is being built with root:root as the owner.
|
mkdir -p docker_node_modules
|
||||||
mkdir -p ~/.npm_cacache
|
|
||||||
|
|
||||||
# https://github.com/docker/compose/issues/4725
|
# https://github.com/docker/compose/issues/4725
|
||||||
docker-compose build
|
docker-compose build
|
||||||
# Create .npm directory in the container, since it is not yet present and we need it for next step.
|
|
||||||
docker-compose run --user="$UID" --rm --service-ports test mkdir -p /opt/sealious-app/.npm
|
|
||||||
# Link the host-bound npm cache directory into the container's npm cache directory.
|
|
||||||
docker-compose run --user="$UID" --rm --service-ports test ln -s /opt/sealious-app/.npm_cacache/ /opt/sealious-app/.npm/_cacache
|
|
||||||
docker-compose up -d db
|
docker-compose up -d db
|
||||||
./npm.sh --no-TTY --user="$UID" ci && ./npm.sh --no-TTY --user="$UID" run build
|
./npm.sh --no-TTY ci && ./npm.sh --no-TTY run build
|
||||||
|
|
||||||
rm -f log.html
|
rm -f log.html
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash -xe
|
#!/bin/bash -xe
|
||||||
|
echo "starting sanity test..."
|
||||||
export SEALIOUS_PORT=$PORT
|
export SEALIOUS_PORT=$PORT
|
||||||
SEALIOUS_BASE_URL=$(cat .base_url)
|
SEALIOUS_BASE_URL=$(cat .base_url)
|
||||||
export SEALIOUS_BASE_URL
|
export SEALIOUS_BASE_URL
|
||||||
@ -14,4 +14,3 @@ docker-compose run --user="$UID" \
|
|||||||
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
||||||
-e "SEALIOUS_SANITY=true" \
|
-e "SEALIOUS_SANITY=true" \
|
||||||
test
|
test
|
||||||
|
|
||||||
|
9
mocha_root_hooks.js
Normal file
9
mocha_root_hooks.js
Normal file
@ -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();
|
||||||
|
},
|
||||||
|
};
|
18
npm.sh
18
npm.sh
@ -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
|
||||||
|
13873
package-lock.json
generated
13873
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -5,29 +5,41 @@
|
|||||||
"main": "./dist/back/index.js",
|
"main": "./dist/back/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "docker-compose up -d db && node .",
|
"start": "docker-compose up -d db && node .",
|
||||||
"typecheck:back": "tsc --noEmit -p src/back",
|
"typecheck:back": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/back",
|
||||||
"typecheck:front": "tsc --noEmit -p src/front",
|
"typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/front",
|
||||||
"build": "sealgen build",
|
"build": "sealgen build",
|
||||||
"watch": "multiple-scripts-tmux \"npm run typecheck:back -- --watch\" \"SEALIOUS_PORT=$SEALIOUS_PORT SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL nodemon --enable-source-maps .\" \"npm run build -- --watch\" \"npm run typecheck:front -- --watch\" ",
|
"watch": "multiple-scripts-tmux -p watch",
|
||||||
"test": "TS_NODE_PROJECT='./src/back/tsconfig.json' mocha --recursive --require ts-node/register src/back/app.ts src/back/**/*.test.ts src/back/**/**/*.test.ts src/back/**/**/**/*.test.ts src/back/**/**/**/**/*.test.ts",
|
"reset-db": "docker-compose down && docker-compose up -d",
|
||||||
|
"install-test-deps": "npx playwright install firefox",
|
||||||
|
"test": "npm run install-test-deps && TS_NODE_PROJECT='./src/back/tsconfig.json' mocha --recursive --timeout 20000 --require ts-node/register --require mocha_root_hooks.js src/back/app.ts src/back/**/*.test.ts src/back/**/**/*.test.ts src/back/**/**/**/*.test.ts src/back/**/**/**/**/*.test.ts",
|
||||||
"coverage": "nyc npm run test --",
|
"coverage": "nyc npm run test --",
|
||||||
|
"postinstall": "sealgen make-env",
|
||||||
"test-reports": "docker-compose up -d && ./npm.sh run coverage -- --reporter xunit --reporter-option output=.xunit",
|
"test-reports": "docker-compose up -d && ./npm.sh run coverage -- --reporter xunit --reporter-option output=.xunit",
|
||||||
"show-coverage": "npm run test-reports; xdg-open coverage/index.html"
|
"show-coverage": "npm run test-reports; xdg-open coverage/index.html"
|
||||||
},
|
},
|
||||||
|
"tmux-scripts": {
|
||||||
|
"watch": [
|
||||||
|
"npm run typecheck:back -- --watch",
|
||||||
|
"npm run build -- --watch",
|
||||||
|
"npm run typecheck:front -- --watch",
|
||||||
|
"SEALIOUS_PORT=$SEALIOUS_PORT SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL nodemon --enable-source-maps ."
|
||||||
|
]
|
||||||
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@hotwired/turbo": "^7.1.0",
|
"@hotwired/turbo": "^7.1.0",
|
||||||
"@koa/router": "^10.1.1",
|
"@koa/router": "^10.1.1",
|
||||||
|
"@playwright/test": "^1.36.1",
|
||||||
"@sealcode/sealgen": "^0.8.39",
|
"@sealcode/sealgen": "^0.8.39",
|
||||||
"@sealcode/ts-predicates": "^0.4.0",
|
"@sealcode/ts-predicates": "^0.4.3",
|
||||||
"@types/kill-port": "^2.0.0",
|
"@types/kill-port": "^2.0.0",
|
||||||
"hint": "^7.0.1",
|
"hint": "^7.0.1",
|
||||||
"locreq": "^2.0.2",
|
"locreq": "^2.0.2",
|
||||||
"multiple-scripts-tmux": "^1.0.4",
|
"multiple-scripts-tmux": "^1.0.4",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^3.0.1",
|
||||||
"sealious": "^0.17.21",
|
"sealious": "^0.17.33",
|
||||||
"stimulus": "^2.0.0",
|
"stimulus": "^2.0.0",
|
||||||
"tempstream": "^0.0.21"
|
"tempstream": "^0.0.21"
|
||||||
},
|
},
|
||||||
@ -39,7 +51,8 @@
|
|||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@sealcode/ansi-html-stream": "^1.0.1",
|
"@sealcode/ansi-html-stream": "^1.0.1",
|
||||||
"@types/koa__router": "^8.0.4",
|
"@types/koa__router": "^8.0.4",
|
||||||
"@types/mocha": "^9.1.0",
|
"@types/mocha": "^9.1.1",
|
||||||
|
"@types/node": "^20.8.4",
|
||||||
"@types/tedious": "^4.0.7",
|
"@types/tedious": "^4.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.2",
|
"@typescript-eslint/parser": "^5.10.2",
|
||||||
|
@ -2,6 +2,7 @@ import _locreq from "locreq";
|
|||||||
import { default as Sealious, App, LoggerMailer, SMTPMailer } from "sealious";
|
import { default as Sealious, App, LoggerMailer, SMTPMailer } from "sealious";
|
||||||
import { LoggerLevel } from "sealious/@types/src/app/logger";
|
import { LoggerLevel } from "sealious/@types/src/app/logger";
|
||||||
import { collections } from "./collections/collections";
|
import { collections } from "./collections/collections";
|
||||||
|
import ADMIN_CREDENTIALS from "./default-admin-credentials";
|
||||||
const locreq = _locreq(__dirname);
|
const locreq = _locreq(__dirname);
|
||||||
|
|
||||||
const PORT = process.env.SEALIOUS_PORT ? parseInt(process.env.SEALIOUS_PORT) : 8080;
|
const PORT = process.env.SEALIOUS_PORT ? parseInt(process.env.SEALIOUS_PORT) : 8080;
|
||||||
@ -47,7 +48,7 @@ export default class TheApp extends App {
|
|||||||
version: "0.0.1",
|
version: "0.0.1",
|
||||||
default_language: "en",
|
default_language: "en",
|
||||||
base_url,
|
base_url,
|
||||||
admin_email: "admin@example.com",
|
admin_email: ADMIN_CREDENTIALS.email,
|
||||||
colors: {
|
colors: {
|
||||||
primary: "#5294a1",
|
primary: "#5294a1",
|
||||||
},
|
},
|
||||||
|
@ -5,6 +5,7 @@ import _GroupsToUsers from "./groups-to-users";
|
|||||||
import _Groups from "./groups";
|
import _Groups from "./groups";
|
||||||
import _PasswordResetIntents from "./password-reset-intents";
|
import _PasswordResetIntents from "./password-reset-intents";
|
||||||
import _Secrets from "./secrets";
|
import _Secrets from "./secrets";
|
||||||
|
import _Tasks from "./tasks";
|
||||||
import _UserRoles from "./user-roles";
|
import _UserRoles from "./user-roles";
|
||||||
import _Users from "./users";
|
import _Users from "./users";
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ export const GroupsToUsers = new _GroupsToUsers();
|
|||||||
export const Groups = new _Groups();
|
export const Groups = new _Groups();
|
||||||
export const PasswordResetIntents = new _PasswordResetIntents();
|
export const PasswordResetIntents = new _PasswordResetIntents();
|
||||||
export const Secrets = new _Secrets();
|
export const Secrets = new _Secrets();
|
||||||
|
export const Tasks = new _Tasks();
|
||||||
export const UserRoles = new _UserRoles();
|
export const UserRoles = new _UserRoles();
|
||||||
export const Users = new _Users();
|
export const Users = new _Users();
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ export const collections = {
|
|||||||
groups: Groups,
|
groups: Groups,
|
||||||
"password-reset-intents": PasswordResetIntents,
|
"password-reset-intents": PasswordResetIntents,
|
||||||
secrets: Secrets,
|
secrets: Secrets,
|
||||||
|
tasks: Tasks,
|
||||||
"user-roles": UserRoles,
|
"user-roles": UserRoles,
|
||||||
users: Users,
|
users: Users,
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,8 @@ import assert from "assert";
|
|||||||
import TheApp from "../app";
|
import TheApp from "../app";
|
||||||
import { withProdApp } from "../test_utils/with-prod-app";
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
|
||||||
describe("password-reset-intents", () => {
|
describe("password-reset-intents", function () {
|
||||||
|
//ts-ignore
|
||||||
async function createAUser(app: TheApp) {
|
async function createAUser(app: TheApp) {
|
||||||
await app.collections.users.suCreate({
|
await app.collections.users.suCreate({
|
||||||
username: "user",
|
username: "user",
|
||||||
@ -13,8 +14,8 @@ describe("password-reset-intents", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it("tells you if the email address doesn't exist", async () =>
|
it("tells you if the email address doesn't exist", async function () {
|
||||||
withProdApp(async ({ app, base_url }) => {
|
return withProdApp(async ({ app, base_url }) => {
|
||||||
const email = "fake@example.com";
|
const email = "fake@example.com";
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
@ -31,7 +32,8 @@ describe("password-reset-intents", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error("it didn't throw");
|
throw new Error("it didn't throw");
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows anyone to create an intent, if the email exists", async () =>
|
it("allows anyone to create an intent, if the email exists", async () =>
|
||||||
withProdApp(async ({ app, base_url }) => {
|
withProdApp(async ({ app, base_url }) => {
|
||||||
|
21
src/back/collections/tasks.ts
Normal file
21
src/back/collections/tasks.ts
Normal file
@ -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();
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
import { App, Collections, Context, FieldTypes, Policies } from "sealious";
|
import { App, Collections, Context, FieldTypes, Policies } from "sealious";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import TheApp from "../app";
|
import TheApp from "../app";
|
||||||
|
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||||
|
|
||||||
export default class Users extends Collections.users {
|
export default class Users extends Collections.users {
|
||||||
fields = {
|
fields = {
|
||||||
...App.BaseCollections.users.fields,
|
...App.BaseCollections.users.fields,
|
||||||
email: new FieldTypes.Email().setRequired(true),
|
email: new FieldTypes.Email().setRequired(true),
|
||||||
|
username: new FieldTypes.Username().setRequired(true),
|
||||||
roles: new FieldTypes.ReverseSingleReference({
|
roles: new FieldTypes.ReverseSingleReference({
|
||||||
referencing_collection: "user-roles",
|
referencing_collection: "user-roles",
|
||||||
referencing_field: "user",
|
referencing_field: "user",
|
||||||
@ -18,7 +20,7 @@ export default class Users extends Collections.users {
|
|||||||
assert(app instanceof TheApp);
|
assert(app instanceof TheApp);
|
||||||
await super.init(app, name);
|
await super.init(app, name);
|
||||||
app.on("started", async () => {
|
app.on("started", async () => {
|
||||||
const username = "admin";
|
const username = ADMIN_CREDENTIALS.username;
|
||||||
const users = await app.collections.users
|
const users = await app.collections.users
|
||||||
.suList()
|
.suList()
|
||||||
.filter({ username })
|
.filter({ username })
|
||||||
@ -30,8 +32,8 @@ export default class Users extends Collections.users {
|
|||||||
);
|
);
|
||||||
await app.collections.users.suCreate({
|
await app.collections.users.suCreate({
|
||||||
username,
|
username,
|
||||||
password: "adminadmin",
|
password: ADMIN_CREDENTIALS.password,
|
||||||
email: "admin@example.com",
|
email: ADMIN_CREDENTIALS.email,
|
||||||
roles: [],
|
roles: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
7
src/back/default-admin-credentials.ts
Normal file
7
src/back/default-admin-credentials.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const ADMIN_CREDENTIALS = {
|
||||||
|
username: "admin",
|
||||||
|
password: "adminadmin",
|
||||||
|
email: "admin@example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ADMIN_CREDENTIALS;
|
@ -1,4 +1,4 @@
|
|||||||
import { App, EmailTemplates, Errors } from "sealious";
|
import { EmailTemplates, Errors } from "sealious";
|
||||||
import TheApp from "../app";
|
import TheApp from "../app";
|
||||||
|
|
||||||
export default async function PasswordResetTemplate(
|
export default async function PasswordResetTemplate(
|
||||||
@ -18,7 +18,7 @@ export default async function PasswordResetTemplate(
|
|||||||
|
|
||||||
return EmailTemplates.Simple(app, {
|
return EmailTemplates.Simple(app, {
|
||||||
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),
|
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),
|
||||||
to: `${username}<${email_address}>`,
|
to: `${String(username)}<${email_address}>`,
|
||||||
text: `
|
text: `
|
||||||
${app.i18n("password_reset_email_text", [app.manifest.name, username])}`,
|
${app.i18n("password_reset_email_text", [app.manifest.name, username])}`,
|
||||||
buttons: [
|
buttons: [
|
||||||
|
@ -10,7 +10,7 @@ kill(app.config["www-server"].port)
|
|||||||
.then(() => app.start())
|
.then(() => app.start())
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
if (process.env.SEALIOUS_SANITY === "true") {
|
if (process.env.SEALIOUS_SANITY === "true") {
|
||||||
console.log("Exiting with error code 0");
|
console.error("Exiting with error code 0");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
mainRouter(app.HTTPServer.router);
|
mainRouter(app.HTTPServer.router);
|
||||||
@ -18,7 +18,7 @@ kill(app.config["www-server"].port)
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (process.env.SEALIOUS_SANITY === "true") {
|
if (process.env.SEALIOUS_SANITY === "true") {
|
||||||
console.log("EXITING WITH STATUS 1");
|
console.error("EXITING WITH STATUS 1");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BaseContext } from "koa";
|
import { BaseContext } from "koa";
|
||||||
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
||||||
import { Collection, Errors } from "sealious";
|
import { Collection, Errors } from "sealious";
|
||||||
import { ItemFields } from "sealious/@types/src/chip-types/collection-item-body";
|
import ItemFields from "sealious/@types/src/chip-types/collection-item-body";
|
||||||
|
|
||||||
export interface CollectionTiedFormData<C extends Collection> {
|
export interface CollectionTiedFormData<C extends Collection> {
|
||||||
values: Partial<{ [field in keyof ItemFields<C>]: string }>;
|
values: Partial<{ [field in keyof ItemFields<C>]: string }>;
|
||||||
|
@ -1,6 +1,23 @@
|
|||||||
import { BaseContext } from "koa";
|
import { BaseContext } from "koa";
|
||||||
|
import { SignUpURL, SignInURL, TodoURL, LogoutURL } from "../urls";
|
||||||
|
|
||||||
export default async function navbar(ctx: BaseContext) {
|
export default async function navbar(ctx: BaseContext) {
|
||||||
|
const isLoggedIn = !!ctx.$context.session_id;
|
||||||
|
|
||||||
|
const linkData = isLoggedIn
|
||||||
|
? [
|
||||||
|
{ text: "Logout", url: LogoutURL },
|
||||||
|
{ text: "To do app", url: TodoURL },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ text: "Sign in", url: SignInURL },
|
||||||
|
{ text: "Sign up", url: SignUpURL },
|
||||||
|
];
|
||||||
|
|
||||||
|
const linksHTML = linkData
|
||||||
|
.map((link) => `<li><a href="${link.url}">${link.text}</a></li>`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
return /* HTML */ ` <nav>
|
return /* HTML */ ` <nav>
|
||||||
<a href="/" class="nav-logo">
|
<a href="/" class="nav-logo">
|
||||||
<img
|
<img
|
||||||
@ -12,7 +29,7 @@ export default async function navbar(ctx: BaseContext) {
|
|||||||
Sealious App
|
Sealious App
|
||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/logowanie">Logowanie</a></li>
|
${linksHTML}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>`;
|
</nav>`;
|
||||||
}
|
}
|
||||||
|
53
src/back/routes/common/tasks-view.ts
Normal file
53
src/back/routes/common/tasks-view.ts
Normal file
@ -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>
|
||||||
|
`;
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import { withProdApp } from "../test_utils/with-prod-app";
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
import { LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||||
import { HelloURL } from "./urls";
|
import { HelloURL } from "./urls";
|
||||||
|
|
||||||
describe("Hello", () => {
|
describe("Hello", () => {
|
||||||
it("doesn't crash", async function () {
|
it("doesn't crash", async function () {
|
||||||
this.timeout(LONG_TEST_TIMEOUT);
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
return withProdApp(async ({ base_url, rest_api }) => {
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
await rest_api.get(HelloURL);
|
await rest_api.get(HelloURL);
|
||||||
await webhintURL(base_url + HelloURL);
|
await webhintURL(base_url + HelloURL);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { webhintURL } from "../test_utils/webhint";
|
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||||
import { withProdApp } from "../test_utils/with-prod-app";
|
import { withProdApp } from "../test_utils/with-prod-app";
|
||||||
|
|
||||||
describe("homepage", function () {
|
describe("homepage", function () {
|
||||||
this.timeout(200000);
|
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
it("passes webhint tests", () =>
|
it("passes webhint tests", () =>
|
||||||
withProdApp(async ({ base_url }) => {
|
withProdApp(async ({ base_url }) => {
|
||||||
await webhintURL(`${base_url}/`);
|
await webhintURL(`${base_url}/`);
|
||||||
|
30
src/back/routes/logout.redirect.ts
Normal file
30
src/back/routes/logout.redirect.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
55
src/back/routes/logout.test.ts
Normal file
55
src/back/routes/logout.test.ts
Normal file
@ -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}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -5,7 +5,15 @@ import { mount } from "@sealcode/sealgen";
|
|||||||
import * as URLs from "./urls";
|
import * as URLs from "./urls";
|
||||||
|
|
||||||
import { default as Hello } from "./hello.page";
|
import { default as Hello } from "./hello.page";
|
||||||
|
import { default as Logout } from "./logout.redirect";
|
||||||
|
import { default as SignIn } from "./signIn.form";
|
||||||
|
import { default as SignUp } from "./signUp.form";
|
||||||
|
import { default as Todo } from "./todo.form";
|
||||||
|
|
||||||
export default function mountAutoRoutes(router: Router) {
|
export default function mountAutoRoutes(router: Router) {
|
||||||
mount(router, URLs.HelloURL, Hello);
|
mount(router, URLs.HelloURL, Hello);
|
||||||
|
mount(router, URLs.LogoutURL, Logout);
|
||||||
|
mount(router, URLs.SignInURL, SignIn);
|
||||||
|
mount(router, URLs.SignUpURL, SignUp);
|
||||||
|
mount(router, URLs.TodoURL, Todo);
|
||||||
}
|
}
|
||||||
|
121
src/back/routes/signIn.form.ts
Normal file
121
src/back/routes/signIn.form.ts
Normal file
@ -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)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
103
src/back/routes/signIn.test.ts
Normal file
103
src/back/routes/signIn.test.ts
Normal file
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
139
src/back/routes/signUp.form.ts
Normal file
139
src/back/routes/signUp.form.ts
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
})();
|
122
src/back/routes/signUp.test.ts
Normal file
122
src/back/routes/signUp.test.ts
Normal file
@ -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}"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
107
src/back/routes/todo.form.ts
Normal file
107
src/back/routes/todo.form.ts
Normal file
@ -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)}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
81
src/back/routes/todo.test.ts
Normal file
81
src/back/routes/todo.test.ts
Normal file
@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
16
src/back/test_utils/browser-creator.ts
Normal file
16
src/back/test_utils/browser-creator.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,8 @@ import { spawn } from "child_process";
|
|||||||
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
export const LONG_TEST_TIMEOUT = 30 * 1000;
|
export const LONG_TEST_TIMEOUT = 100 * 1000;
|
||||||
|
export const VERY_LONG_TEST_TIMEOUT = 75 * 1000;
|
||||||
|
|
||||||
export async function webhintURL(url: string, config = locreq.resolve(".hintrc")) {
|
export async function webhintURL(url: string, config = locreq.resolve(".hintrc")) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import TheApp from "../app";
|
import TheApp from "../app";
|
||||||
import { mainRouter } from "../routes";
|
import { mainRouter } from "../routes";
|
||||||
import _locreq from "locreq";
|
import _locreq from "locreq";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
const locreq = _locreq(__dirname);
|
const locreq = _locreq(__dirname);
|
||||||
import Sealious, { SMTPMailer } from "sealious";
|
import { SMTPMailer } from "sealious";
|
||||||
import { TestUtils } from "sealious";
|
import { TestUtils } from "sealious";
|
||||||
|
|
||||||
export async function withProdApp(
|
export async function withProdApp(
|
||||||
@ -21,7 +22,7 @@ export async function withProdApp(
|
|||||||
app.config.datastore_mongo = {
|
app.config.datastore_mongo = {
|
||||||
host: "db",
|
host: "db",
|
||||||
port: 27017,
|
port: 27017,
|
||||||
db_name: "sealious-app-test",
|
db_name: "sealious-app-test" + uuid(),
|
||||||
};
|
};
|
||||||
app.config.logger.level = <const>"none";
|
app.config.logger.level = <const>"none";
|
||||||
app.mailer = new SMTPMailer({
|
app.mailer = new SMTPMailer({
|
||||||
@ -31,11 +32,11 @@ export async function withProdApp(
|
|||||||
password: "any",
|
password: "any",
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.start();
|
|
||||||
mainRouter(app.HTTPServer.router);
|
mainRouter(app.HTTPServer.router);
|
||||||
|
|
||||||
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
||||||
|
|
||||||
|
await app.start();
|
||||||
const base_url = `http://127.0.0.1:${port}`;
|
const base_url = `http://127.0.0.1:${port}`;
|
||||||
const mail_api = new TestUtils.MailcatcherAPI("http://mailcatcher:1080", app);
|
const mail_api = new TestUtils.MailcatcherAPI("http://mailcatcher:1080", app);
|
||||||
await mail_api.deleteAllInstanceEmails();
|
await mail_api.deleteAllInstanceEmails();
|
||||||
@ -52,9 +53,12 @@ export async function withProdApp(
|
|||||||
rest_api: new TestUtils.MockRestApi(base_url),
|
rest_api: new TestUtils.MockRestApi(base_url),
|
||||||
mail_api,
|
mail_api,
|
||||||
});
|
});
|
||||||
|
|
||||||
await stop();
|
await stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await stop();
|
if (app.status !== "stopped") {
|
||||||
|
await stop();
|
||||||
|
}
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"lib": ["es6", "esnext"],
|
"lib": ["es6", "esnext"],
|
||||||
"outDir": "../../dist/back",
|
"outDir": "../../dist/back",
|
||||||
"keyofStringsOnly": true,
|
"keyofStringsOnly": true,
|
||||||
"checkJs": true,
|
"checkJs": false,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
30
src/front/controllers/task-controller.ts
Normal file
30
src/front/controllers/task-controller.ts
Normal file
@ -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);
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"lib": ["dom"]
|
"lib": ["dom"],
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["./**/*", "./index.ts"]
|
"include": ["./**/*", "./index.ts"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user