├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .env.local.secret ├── .env.playwright.secret ├── .env.template ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitsecret ├── keys │ ├── pubring.kbx │ └── trustdb.gpg └── paths │ └── mapping.cfg ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .stylelintignore ├── .stylelintrc.cjs ├── .tool-versions ├── .vscode ├── css-custom-data-tailwind.json ├── extensions.template.json ├── launch.template.json └── settings.template.json ├── LICENSE ├── README.md ├── docker-compose.local.api.yml ├── docker-compose.local.app-base.yml ├── docker-compose.local.app-only.yml ├── docker-compose.local.app-playwright.yml ├── docker-compose.local.e2e.yml ├── docker-compose.local.infra-only.yml ├── docker ├── app │ └── Dockerfile └── playwright │ └── Dockerfile ├── package-lock.json ├── package.json ├── playwright.api.config.ts ├── playwright.common.config.ts ├── playwright.e2e.config.ts ├── postcss.config.cjs ├── prisma ├── migrations │ ├── 20231014170017_initial │ │ └── migration.sql │ ├── 20231215143702_create_code_snippet_model │ │ └── migration.sql │ ├── 20240103162852_add_is_deleted_code_snippet_property │ │ └── migration.sql │ ├── 20240103163750_add_deleted_at_code_snippet_property │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── renovate.json ├── reports ├── playwright │ └── .gitkeep └── vitest │ └── .gitkeep ├── scripts ├── app │ ├── lib │ │ ├── lucia │ │ │ └── client.js │ │ ├── prisma │ │ │ └── client.js │ │ └── utils │ │ │ ├── env.js │ │ │ └── errors.js │ └── seeds │ │ ├── enumeration │ │ ├── deleteAll.js │ │ ├── seedCodeSnippets.js │ │ └── seedUsersAndSessions.js │ │ └── groups │ │ ├── development.js │ │ └── playwright.js ├── docker │ └── images │ │ ├── app │ │ ├── build.sh │ │ └── modes │ │ │ └── maintenance │ │ │ └── run.sh │ │ └── postgres │ │ └── exec-psql-connect.sh ├── northflank │ └── forwarding │ │ ├── postgres-production.sh │ │ └── postgres-staging.sh ├── stack │ ├── _lib │ │ ├── actions.js │ │ └── utils │ │ │ ├── docker.js │ │ │ ├── env.js │ │ │ ├── misc.js │ │ │ └── paths.js │ └── local │ │ ├── all │ │ ├── _lib │ │ │ └── constants.js │ │ ├── api │ │ │ ├── _lib │ │ │ │ └── constants.js │ │ │ ├── down.js │ │ │ └── headless.js │ │ └── e2e │ │ │ ├── _lib │ │ │ └── constants.js │ │ │ ├── down.js │ │ │ └── headless.js │ │ ├── infra-app │ │ ├── _lib │ │ │ └── constants.js │ │ ├── playwright │ │ │ ├── _lib │ │ │ │ └── constants.js │ │ │ ├── api │ │ │ │ ├── headless.js │ │ │ │ └── ui.js │ │ │ ├── down.js │ │ │ └── e2e │ │ │ │ ├── codegen.js │ │ │ │ ├── headed.js │ │ │ │ ├── headless.js │ │ │ │ └── ui.js │ │ └── stack-only │ │ │ ├── _lib │ │ │ └── constants.js │ │ │ ├── down.js │ │ │ ├── infra-app-seed.js │ │ │ └── infra-app.js │ │ └── infra │ │ ├── _lib │ │ └── constants.js │ │ ├── playwright │ │ ├── _lib │ │ │ └── constants.js │ │ ├── api │ │ │ ├── headless.js │ │ │ └── ui.js │ │ ├── down.js │ │ ├── e2e │ │ │ ├── codegen.js │ │ │ ├── headed.js │ │ │ ├── headless.js │ │ │ └── ui.js │ │ └── infra-app.js │ │ └── stack-only │ │ ├── _lib │ │ └── constants.js │ │ ├── app.js │ │ ├── down.js │ │ ├── infra-app.js │ │ ├── infra-migrate.js │ │ ├── infra.js │ │ ├── migrate.js │ │ └── seed.js └── testing │ └── load │ ├── commands │ ├── run-sh.sh │ └── run.sh │ └── tests │ ├── _template.load-test.js │ └── load-1.load-test.js ├── src ├── app.d.ts ├── app.html ├── app.postcss ├── hooks.client.ts ├── hooks.server.ts ├── lib │ ├── client │ │ ├── components │ │ │ ├── app-shell │ │ │ │ ├── AppBar.svelte │ │ │ │ ├── AppBar.svelte.dom-test.ts │ │ │ │ ├── AppMenuButton.svelte │ │ │ │ ├── AppMenuButton.svelte.dom-test.ts │ │ │ │ ├── AppShell.svelte │ │ │ │ ├── AppShell.svelte.dom-test.ts │ │ │ │ ├── Error.svelte │ │ │ │ ├── Error.svelte.dom-test.ts │ │ │ │ ├── PageMessage.svelte │ │ │ │ ├── PageMessage.svelte.dom-test.ts │ │ │ │ └── index.ts │ │ │ ├── code-snippets │ │ │ │ ├── CodeSnippetCard.svelte │ │ │ │ ├── CodeSnippetCard.svelte.dom-test.ts │ │ │ │ ├── CodeSnippetCreateEditForm.svelte │ │ │ │ ├── CodeSnippetCreateEditForm.svelte.dom-test.ts │ │ │ │ ├── CodeSnippetFindForm.svelte │ │ │ │ ├── CodeSnippetFindForm.svelte.dom-test.ts │ │ │ │ └── index.ts │ │ │ ├── common │ │ │ │ ├── Alert.svelte │ │ │ │ ├── Alert.svelte.dom-test.ts │ │ │ │ ├── Card.svelte │ │ │ │ ├── Card.svelte.dom-test.ts │ │ │ │ ├── SimplePaginator.svelte │ │ │ │ ├── SimplePaginator.svelte.dom-test.ts │ │ │ │ ├── SingleCardPageContainer.svelte │ │ │ │ ├── SingleCardPageContainer.svelte.dom-test.ts │ │ │ │ └── index.ts │ │ │ └── testing │ │ │ │ ├── SlotTest.svelte │ │ │ │ └── index.ts │ │ ├── core │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ ├── stores │ │ │ │ ├── index.ts │ │ │ │ ├── previous-app-page.store.dom-test.ts │ │ │ │ ├── previous-app-page.store.ts │ │ │ │ └── testing │ │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ ├── index.ts │ │ │ │ ├── linting.utils.ts │ │ │ │ ├── navigation.utils.dom-test.ts │ │ │ │ └── navigation.utils.ts │ │ ├── global-messages │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ ├── index.dom-test.ts │ │ │ │ └── index.ts │ │ ├── logging │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ ├── posthog │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── posthog-default-page-events-capture.configurator.dom-test.ts │ │ │ ├── posthog-default-page-events-capture.configurator.ts │ │ │ ├── posthog-user-identity.configurator.dom-test.ts │ │ │ ├── posthog-user-identity.configurator.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ ├── sentry │ │ │ ├── index.ts │ │ │ ├── sentry-user-identity.configurator.dom-test.ts │ │ │ ├── sentry-user-identity.configurator.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ ├── skeleton │ │ │ └── utils │ │ │ │ ├── index.ts │ │ │ │ └── setup.utils.ts │ │ └── superforms │ │ │ └── types │ │ │ └── index.ts │ ├── server │ │ ├── code-snippets │ │ │ ├── form-actions │ │ │ │ ├── code-snippets.form-actions.node-test.ts │ │ │ │ ├── code-snippets.form-actions.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── services │ │ │ │ ├── code-snippets.service.node-test.ts │ │ │ │ ├── code-snippets.service.ts │ │ │ │ └── index.ts │ │ │ ├── singletons.ts │ │ │ └── utils │ │ │ │ ├── errors.node-test.ts │ │ │ │ ├── errors.ts │ │ │ │ └── index.ts │ │ ├── core │ │ │ ├── config │ │ │ │ └── index.ts │ │ │ ├── hooks │ │ │ │ ├── check-mandatory-private-env-vars.handle.node-test.ts │ │ │ │ ├── check-mandatory-private-env-vars.handle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── maintenance-mode.handle.node-test.ts │ │ │ │ └── maintenance-mode.handle.ts │ │ │ └── utils │ │ │ │ ├── env.utils.node-test.ts │ │ │ │ ├── env.utils.ts │ │ │ │ ├── index.ts │ │ │ │ ├── url.utils.node-test.ts │ │ │ │ └── url.utils.ts │ │ ├── lucia │ │ │ ├── client.ts │ │ │ ├── guards │ │ │ │ ├── auth-user.guard.node-test.ts │ │ │ │ ├── auth-user.guard.ts │ │ │ │ └── index.ts │ │ │ ├── hooks │ │ │ │ ├── add-auth-data-to-local.handle.node-test.ts │ │ │ │ ├── add-auth-data-to-local.handle.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth │ │ │ │ ├── constants.ts │ │ │ │ ├── google │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── provider.ts │ │ │ │ │ └── utils │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── sign-in.utils.node-test.ts │ │ │ │ │ │ └── sign-in.utils.ts │ │ │ │ └── index.ts │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ ├── index.node-test.ts │ │ │ │ └── index.ts │ │ ├── pagination │ │ │ ├── types │ │ │ │ ├── index.ts │ │ │ │ └── pagination-query.ts │ │ │ └── utils │ │ │ │ ├── index.node-test.ts │ │ │ │ └── index.ts │ │ ├── posthog │ │ │ ├── client.ts │ │ │ └── index.ts │ │ ├── prisma │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── roarr │ │ │ ├── client.ts │ │ │ ├── hooks │ │ │ │ ├── http-log.handle.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ ├── sentry │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── set-sentry-user-identity.handle.node-test.ts │ │ │ │ └── set-sentry-user-identity.handle.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ ├── superforms │ │ │ └── testing │ │ │ │ └── index.ts │ │ └── sveltekit │ │ │ └── testing │ │ │ └── index.ts │ └── shared │ │ ├── code-snippets │ │ ├── dtos │ │ │ ├── create-edit-code-snippet.dto.node-test.ts │ │ │ ├── create-edit-code-snippet.dto.ts │ │ │ ├── delete-code-snippet.dto.node-test.ts │ │ │ ├── delete-code-snippet.dto.ts │ │ │ ├── find-code-snippets.dto.node-test.ts │ │ │ ├── find-code-snippets.dto.ts │ │ │ └── index.ts │ │ └── testing │ │ │ └── index.ts │ │ ├── core │ │ ├── testing │ │ │ └── index.ts │ │ └── utils │ │ │ ├── datetime.utils.node-test.ts │ │ │ ├── datetime.utils.ts │ │ │ ├── index.ts │ │ │ ├── parsing.utils.node-test.ts │ │ │ ├── parsing.utils.ts │ │ │ ├── url.utils.node-test.ts │ │ │ └── url.utils.ts │ │ ├── logging │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ └── index.ts │ │ ├── lucia │ │ ├── testing │ │ │ └── index.ts │ │ └── types │ │ │ └── index.ts │ │ ├── posthog │ │ ├── constants │ │ │ └── index.ts │ │ └── utils │ │ │ ├── index.node-test.ts │ │ │ └── index.ts │ │ ├── sentry │ │ ├── client.node-test.ts │ │ ├── client.ts │ │ ├── index.ts │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ └── index.ts │ │ ├── superforms │ │ └── testing │ │ │ └── index.ts │ │ ├── sveltekit │ │ └── testing │ │ │ ├── index.ts │ │ │ └── test-helpers.ts │ │ └── zod │ │ ├── testing │ │ └── index.ts │ │ └── utils │ │ ├── errors.node-test.ts │ │ ├── errors.ts │ │ ├── extract.node-test.ts │ │ ├── extract.ts │ │ └── index.ts ├── routes │ ├── (app) │ │ ├── (card-layout) │ │ │ ├── +layout.svelte │ │ │ ├── code-snippets │ │ │ │ ├── [id] │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── edit │ │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── page.server.node-test.ts │ │ │ │ │ │ └── page.svelte.dom-test.ts │ │ │ │ │ ├── page.server.node-test.ts │ │ │ │ │ └── page.svelte.dom-test.ts │ │ │ │ └── create │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── page.server.node-test.ts │ │ │ │ │ └── page.svelte.dom-test.ts │ │ │ ├── layout.svelte.dom-test.ts │ │ │ └── profile │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── page.server.node-test.ts │ │ │ │ └── page.svelte.dom-test.ts │ │ ├── +layout.svelte │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── layout.svelte.dom-test.ts │ │ ├── page.CodeSnippetCard.mock.svelte │ │ ├── page.server.module.node-test.ts │ │ ├── page.server.module.ts │ │ ├── page.server.node-test.ts │ │ └── page.svelte.dom-test.ts │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── api │ │ └── healthcheck │ │ │ ├── +server.ts │ │ │ └── server.node-test.ts │ ├── error.svelte.dom-test.ts │ ├── layout.server.node-test.ts │ ├── layout.svelte.dom-test.ts │ ├── maintenance │ │ ├── +page.server.ts │ │ └── page.server.node-test.ts │ └── sign-in │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── page.server.node-test.ts │ │ └── page.svelte.dom-test.ts └── styles │ └── base.postcss ├── static ├── favicon.png └── fonts │ ├── Inter │ ├── README.md │ └── static │ │ ├── Inter-Bold.ttf │ │ └── Inter-Regular.ttf │ ├── Open_Sans │ ├── README.md │ └── static │ │ ├── OpenSans-Bold.ttf │ │ └── OpenSans-Regular.ttf │ └── Source_Code_Pro │ ├── README.md │ └── static │ ├── SourceCodePro-Bold.ttf │ └── SourceCodePro-Regular.ttf ├── svelte.config.js ├── tailwind.config.ts ├── tests └── playwright │ ├── api │ └── tests │ │ ├── index.api-setup.ts │ │ ├── index.api-teardown.ts │ │ ├── signed-in-user--before-sign-out │ │ └── sign-out.api-test.ts │ │ ├── signed-in-user │ │ └── code-snippets │ │ │ ├── create.api-test.ts │ │ │ ├── delete.api-test.ts │ │ │ └── edit.api-test.ts │ │ └── visitor │ │ └── sign-in.api-test.ts │ ├── common │ ├── lib │ │ ├── config │ │ │ └── index.ts │ │ ├── constants.ts │ │ └── setup.ts │ └── saved-states │ │ └── .gitkeep │ └── e2e │ ├── page-objects │ ├── components │ │ └── navigation-bar.component.ts │ └── pages │ │ ├── code-snippets │ │ ├── create.page.ts │ │ ├── edit.page.ts │ │ └── view-details.page.ts │ │ ├── home.page.ts │ │ ├── profile.page.ts │ │ └── sign-in.page.ts │ └── tests │ ├── index.e2e-setup.ts │ ├── index.e2e-teardown.ts │ ├── signed-in-user--before-sign-out │ └── sign-out.e2e-test.ts │ ├── signed-in-user │ └── code-snippets │ │ ├── create.e2e-test.ts │ │ ├── delete.e2e-test.ts │ │ ├── edit.e2e-test.ts │ │ ├── view-details.e2e-test.ts │ │ └── view.e2e-test.ts │ └── visitor │ ├── code-snippets │ ├── view-details.e2e-test.ts │ └── view.e2e-test.ts │ └── sign-in.e2e-test.ts ├── tsconfig.json ├── vite.config.ts ├── vitest.browser.config.ts ├── vitest.common.config.ts ├── vitest.dom-node.setup.ts ├── vitest.dom.config.ts ├── vitest.dom.setup.ts ├── vitest.node.config.ts ├── vitest.node.setup.ts ├── wallaby.dom.template.cjs └── wallaby.node.template.cjs /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 Chrome version 2 | last 1 Firefox version 3 | last 2 Edge major versions 4 | last 2 Safari major versions 5 | last 2 iOS major versions 6 | Firefox ESR 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .gitattributes 4 | .prettierrc 5 | .eslintrc.cjs 6 | .graphqlrc 7 | .editorconfig 8 | .svelte-kit 9 | .vscode 10 | node_modules 11 | npm-debug 12 | build 13 | package 14 | **/.env 15 | docker-compose*.yml 16 | tests/playwright/common/saved-states/* 17 | 18 | # For documentation purposes 19 | !Dockerfile 20 | !README.md 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js,jsx,cjs,mjs,ts,tsx,json,feature,svelte,css,sass,scss,postcss,html,prisma}] 12 | indent_size = 2 13 | 14 | [*.Caddyfile] 15 | indent_style = tab 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.env.local.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/.env.local.secret -------------------------------------------------------------------------------- /.env.playwright.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/.env.playwright.secret -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # Public 2 | 3 | ## Development and production 4 | 5 | PUBLIC_POSTHOG_PROJECT_API_KEY= 6 | PUBLIC_POSTHOG_API_HOST= 7 | 8 | PUBLIC_SENTRY_ENVIRONMENT= 9 | PUBLIC_SENTRY_DSN= 10 | PUBLIC_SENTRY_ORGANIZATION= 11 | PUBLIC_SENTRY_PROJECT_ID= 12 | 13 | # Private 14 | 15 | ## Development only 16 | 17 | PROJECT_NAME=code-snippet-sharing 18 | 19 | POSTGRES_DATA_VOLUME_NAME=postgres-data-development 20 | 21 | ## Development and production 22 | 23 | ORIGIN=http://localhost:3000 24 | 25 | MAINTENANCE_MODE= 26 | 27 | DATABASE_URL="postgresql://postgres:postgres@localhost:5434/postgres?schema=public" 28 | 29 | GOOGLE_OAUTH_APP_CLIENT_ID= 30 | GOOGLE_OAUTH_APP_CLIENT_SECRET= 31 | GOOGLE_OAUTH_APP_REDIRECT_URI=http://localhost:3000/auth/google/callback 32 | 33 | VITE_SENTRY_ORG= 34 | VITE_SENTRY_PROJECT= 35 | VITE_SENTRY_AUTH_TOKEN= 36 | 37 | ROARR_LOG=true 38 | ROARR_MIN_LOG_LEVEL=info 39 | ROARR_SHOW_DEBUG_CONTEXT=false 40 | ROARR_ACCESS_LOG=false 41 | 42 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | !.env.*.template 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | 16 | node_modules 17 | dist 18 | build 19 | .stryker-tmp 20 | .nyc_output 21 | coverage 22 | target 23 | reports 24 | *.tsbuildinfo 25 | -------------------------------------------------------------------------------- /.gitsecret/keys/pubring.kbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/.gitsecret/keys/pubring.kbx -------------------------------------------------------------------------------- /.gitsecret/keys/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/.gitsecret/keys/trustdb.gpg -------------------------------------------------------------------------------- /.gitsecret/paths/mapping.cfg: -------------------------------------------------------------------------------- 1 | .env.local:abbe9da718d9249cfa0b265678517b558b7fbe9f67c38ad714e2f0d9da51c63e 2 | .env.playwright:cc88f3266a590b9cd526fc40bb2dfd415fa65fc19ebaa7396a342dd10a137567 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | !.env.*.template 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | 16 | node_modules 17 | dist 18 | build 19 | .stryker-tmp 20 | .nyc_output 21 | coverage 22 | target 23 | reports 24 | *.tsbuildinfo 25 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'], 5 | overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }], 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | svelteSortOrder: 'options-scripts-markup-styles', 9 | svelteStrictMode: true, 10 | svelteIndentScriptAndStyle: false, 11 | }; 12 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | !.env.*.template 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | 16 | node_modules 17 | dist 18 | build 19 | .stryker-tmp 20 | .nyc_output 21 | coverage 22 | target 23 | reports 24 | *.tsbuildinfo 25 | -------------------------------------------------------------------------------- /.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: [ 5 | 'stylelint-config-recommended', 6 | /* 7 | * Prettier plugin is not needed 8 | * https://stylelint.io/migration-guide/to-15/#deprecated-stylistic-rules 9 | */ 10 | ], 11 | rules: { 12 | 'no-descending-specificity': null, 13 | 'at-rule-no-unknown': [ 14 | true, 15 | { 16 | ignoreAtRules: ['tailwind', 'layer', 'apply', 'config'], 17 | }, 18 | ], 19 | }, 20 | overrides: [ 21 | { 22 | files: ['*.svelte', '**/.svelte'], 23 | customSyntax: 'postcss-html', 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.9.0 2 | -------------------------------------------------------------------------------- /.vscode/css-custom-data-tailwind.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@tailwind", 6 | "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities`, and `variants` styles into your CSS." 7 | }, 8 | { 9 | "name": "@layer", 10 | "description": "Use the `@layer` directive to tell Tailwind which “bucket” a set of custom styles belong to. Valid layers are `base`, `components`, and `utilities`." 11 | }, 12 | { 13 | "name": "@apply", 14 | "description": "Use `@apply` to inline any existing utility classes into your own custom CSS." 15 | }, 16 | { 17 | "name": "@config", 18 | "description": "Use the `@config` directive to specify which config file Tailwind should use when compiling that CSS file." 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/extensions.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "formulahendry.auto-close-tag", 4 | "formulahendry.auto-rename-tag", 5 | "moalamri.inline-fold", 6 | "esbenp.prettier-vscode", 7 | "dbaeumer.vscode-eslint", 8 | "stylelint.vscode-stylelint", 9 | "mhutchie.git-graph", 10 | "eamodio.gitlens", 11 | "GitHub.vscode-pull-request-github", 12 | "wallabyjs.console-ninja", 13 | "wallabyjs.quokka-vscode", 14 | "wallabyjs.wallaby-vscode", 15 | "ms-playwright.playwright", 16 | "editorconfig.editorconfig", 17 | "mikestead.dotenv", 18 | "redhat.vscode-yaml", 19 | "ms-azuretools.vscode-docker", 20 | "github.vscode-github-actions", 21 | "bradlc.vscode-tailwindcss", 22 | "Prisma.prisma", 23 | "svelte.svelte-vscode", 24 | "stordahl.sveltekit-snippets", 25 | "proverbialninja.svelte-extractor", 26 | "vunguyentuan.vscode-postcss", 27 | "wix.vscode-import-cost", 28 | "vitest.explorer" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.template.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | // https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes 9 | // https://code.visualstudio.com/docs/nodejs/browser-debugging#_launch-configuration-attributes 10 | "name": "Chrome: Launch (user code only)", 11 | "type": "chrome", 12 | "request": "launch", 13 | 14 | // Universal attributes with non-default values 15 | "webRoot": "${workspaceFolder}", 16 | "smartStep": true, 17 | 18 | // Launch-specific attributes with non-default values 19 | "url": "http://localhost:3000" 20 | }, 21 | { 22 | // https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes 23 | // https://code.visualstudio.com/docs/nodejs/browser-debugging#_launch-configuration-attributes 24 | "name": "Chrome: Launch (with node_modules)", 25 | "type": "chrome", 26 | "request": "launch", 27 | 28 | // Universal attributes with non-default values 29 | "webRoot": "${workspaceFolder}", 30 | 31 | // Launch-specific attributes with non-default values 32 | "url": "http://localhost:3000" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 nodeexx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.local.api.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | container_name: ${PROJECT_NAME:?err}--api 4 | image: ${PROJECT_NAME:?err}--api:latest 5 | build: 6 | dockerfile: ./docker/playwright/Dockerfile 7 | target: api 8 | privileged: true 9 | pid: host 10 | network_mode: host 11 | volumes: 12 | - type: bind 13 | source: './reports/playwright/api' 14 | target: /playwright/reports/playwright/api 15 | env_file: .env.playwright 16 | depends_on: 17 | app: 18 | condition: service_healthy 19 | -------------------------------------------------------------------------------- /docker-compose.local.app-only.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db-migration: 3 | env_file: .env.local 4 | 5 | app: 6 | env_file: .env.local 7 | -------------------------------------------------------------------------------- /docker-compose.local.app-playwright.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db-migration: 3 | env_file: .env.playwright 4 | 5 | app: 6 | env_file: .env.playwright 7 | -------------------------------------------------------------------------------- /docker-compose.local.e2e.yml: -------------------------------------------------------------------------------- 1 | services: 2 | e2e: 3 | container_name: ${PROJECT_NAME:?err}--e2e 4 | image: ${PROJECT_NAME:?err}--e2e:latest 5 | build: 6 | dockerfile: ./docker/playwright/Dockerfile 7 | target: e2e 8 | privileged: true 9 | pid: host 10 | network_mode: host 11 | volumes: 12 | - type: bind 13 | source: './reports/playwright/e2e' 14 | target: /playwright/reports/playwright/e2e 15 | env_file: .env.playwright 16 | depends_on: 17 | app: 18 | condition: service_healthy 19 | -------------------------------------------------------------------------------- /docker-compose.local.infra-only.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | container_name: ${PROJECT_NAME:?err}--postgres 4 | hostname: ${PROJECT_NAME:?err}--postgres 5 | image: postgres:15.3-alpine3.18 6 | # NOTE: To avoid the error "FATAL: role "root" does not exist docker postgres" 7 | user: postgres 8 | ports: 9 | - host_ip: 0.0.0.0 10 | published: 5434 # host 11 | target: 5434 # container 12 | protocol: tcp 13 | mode: host 14 | volumes: 15 | - type: volume 16 | source: postgres-data 17 | target: /var/lib/postgresql/data 18 | environment: 19 | POSTGRES_USER: postgres 20 | POSTGRES_PASSWORD: postgres 21 | POSTGRES_DB: postgres 22 | PGPORT: 5434 23 | healthcheck: 24 | test: ['CMD-SHELL', 'pg_isready'] 25 | interval: 5s 26 | 27 | # DB connection URL: 28 | # postgres://postgres:postgres@${PROJECT_NAME}--postgres:5434/postgres 29 | # Browser URL: 30 | # http://localhost:8080/?pgsql=${PROJECT_NAME}--postgres%3A5434&username=postgres&db=postgres&ns=public&select=keys 31 | adminer: 32 | container_name: ${PROJECT_NAME:?err}--adminer 33 | image: adminer:4.8.1-standalone 34 | ports: 35 | - host_ip: 0.0.0.0 36 | published: 8080 # host 37 | target: 8080 # container 38 | protocol: tcp 39 | mode: host 40 | depends_on: 41 | postgres: 42 | condition: service_healthy 43 | 44 | volumes: 45 | postgres-data: 46 | name: ${PROJECT_NAME:?err}--${POSTGRES_DATA_VOLUME_NAME:?err} 47 | -------------------------------------------------------------------------------- /docker/playwright/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/playwright:v1.39.0-jammy as common 2 | 3 | WORKDIR /playwright 4 | 5 | ENV PATH /playwright/node_modules/.bin:$PATH 6 | 7 | # Get the needed libraries to run Playwright 8 | RUN apt-get update && apt-get -y install \ 9 | libnss3 \ 10 | libatk-bridge2.0-0 \ 11 | libdrm-dev \ 12 | libxkbcommon-dev \ 13 | libgbm-dev \ 14 | libasound-dev \ 15 | libatspi2.0-0 \ 16 | libxshmfence-dev 17 | 18 | COPY \ 19 | package.json \ 20 | package-lock.json \ 21 | .npmrc* \ 22 | ./ 23 | 24 | RUN --mount=type=cache,target=/root/.npm/_cacache,sharing=shared \ 25 | npm ci 26 | 27 | COPY \ 28 | tsconfig.json \ 29 | playwright.common.config.ts \ 30 | ./ 31 | 32 | # NOTE: Needed for seed scripts 33 | COPY ./scripts/app ./scripts/app 34 | COPY ./prisma ./prisma 35 | RUN npx prisma generate 36 | 37 | COPY ./tests/playwright/common ./tests/playwright/common 38 | 39 | FROM common as e2e 40 | 41 | COPY playwright.e2e.config.ts ./ 42 | 43 | COPY ./tests/playwright/e2e ./tests/playwright/e2e 44 | 45 | # NOTE: Useful for debug purposes 46 | # CMD tail -f /dev/null 47 | 48 | # WARN: Be careful with the order of the commands, because scripts rely on the 49 | # exit status of the test command. 50 | CMD npm run test:e2e:only \ 51 | ; exit_status=$? \ 52 | ; chmod a+rwx -R ./reports/playwright/e2e \ 53 | ; exit $exit_status 54 | 55 | FROM common as api 56 | 57 | COPY playwright.api.config.ts ./ 58 | 59 | # NOTE: Some files are required by common setup 60 | COPY ./tests/playwright/e2e/page-objects ./tests/playwright/e2e/page-objects 61 | 62 | COPY ./tests/playwright/api ./tests/playwright/api 63 | 64 | # NOTE: Useful for debug purposes 65 | # CMD tail -f /dev/null 66 | 67 | # WARN: Be careful with the order of the commands, because scripts rely on the 68 | # exit status of the test command. 69 | CMD npm run test:api:only \ 70 | ; exit_status=$? \ 71 | ; chmod a+rwx -R ./reports/playwright/api \ 72 | ; exit $exit_status 73 | -------------------------------------------------------------------------------- /playwright.common.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | const config: Partial = { 10 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 11 | forbidOnly: !!process.env['CI'], 12 | /* Retry on CI only */ 13 | // retries: process.env['CI'] ? 2 : 0, 14 | retries: 2, 15 | /* Opt out of parallel tests on CI. */ 16 | ...(process.env['CI'] ? { workers: 1 } : {}), 17 | /* See https://playwright.dev/docs/api/class-testoptions. */ 18 | use: { 19 | /* Base URL to use in actions like `await page.goto('/')`. */ 20 | baseURL: 'http://localhost:3000', 21 | /* 22 | * Maximum time each action such as `click()` can take. Defaults to 0 (no timeout). 23 | * https://playwright.dev/docs/test-timeouts#advanced-low-level-timeouts 24 | */ 25 | // actionTimeout: 0, 26 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 27 | // trace: "on-first-retry" 28 | trace: { 29 | mode: 'retain-on-failure', 30 | }, 31 | video: { 32 | mode: 'retain-on-failure', 33 | }, 34 | }, 35 | }; 36 | 37 | export default config; 38 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = { 4 | plugins: { 5 | 'postcss-import': {}, 6 | 'tailwindcss/nesting': 'postcss-nesting', 7 | // Some plugins, like tailwindcss/nesting, need to run before Tailwind, 8 | tailwindcss: {}, 9 | 'postcss-preset-env': { 10 | // https://cssdb.org/ 11 | stage: 2, 12 | features: { 'nesting-rules': false }, 13 | }, 14 | // Should be the last plugin 15 | autoprefixer: {}, 16 | }, 17 | }; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /prisma/migrations/20231014170017_initial/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "email_verified" BOOLEAN NOT NULL DEFAULT false, 6 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updated_at" TIMESTAMP(3) NOT NULL, 8 | 9 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "sessions" ( 14 | "id" TEXT NOT NULL, 15 | "user_id" TEXT NOT NULL, 16 | "active_expires" BIGINT NOT NULL, 17 | "idle_expires" BIGINT NOT NULL, 18 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | "updated_at" TIMESTAMP(3) NOT NULL, 20 | 21 | CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") 22 | ); 23 | 24 | -- CreateTable 25 | CREATE TABLE "keys" ( 26 | "id" TEXT NOT NULL, 27 | "hashed_password" TEXT, 28 | "user_id" TEXT NOT NULL, 29 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | "updated_at" TIMESTAMP(3) NOT NULL, 31 | 32 | CONSTRAINT "keys_pkey" PRIMARY KEY ("id") 33 | ); 34 | 35 | -- CreateIndex 36 | CREATE UNIQUE INDEX "users_id_key" ON "users"("id"); 37 | 38 | -- CreateIndex 39 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 40 | 41 | -- CreateIndex 42 | CREATE UNIQUE INDEX "sessions_id_key" ON "sessions"("id"); 43 | 44 | -- CreateIndex 45 | CREATE INDEX "sessions_user_id_idx" ON "sessions"("user_id"); 46 | 47 | -- CreateIndex 48 | CREATE UNIQUE INDEX "keys_id_key" ON "keys"("id"); 49 | 50 | -- CreateIndex 51 | CREATE INDEX "keys_user_id_idx" ON "keys"("user_id"); 52 | 53 | -- AddForeignKey 54 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 55 | 56 | -- AddForeignKey 57 | ALTER TABLE "keys" ADD CONSTRAINT "keys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 58 | -------------------------------------------------------------------------------- /prisma/migrations/20231215143702_create_code_snippet_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "code_snippets" ( 3 | "id" SERIAL NOT NULL, 4 | "user_id" TEXT NOT NULL, 5 | "name" TEXT NOT NULL, 6 | "code" TEXT NOT NULL, 7 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updated_at" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "code_snippets_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "code_snippets_user_id_idx" ON "code_snippets"("user_id"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "code_snippets" ADD CONSTRAINT "code_snippets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20240103162852_add_is_deleted_code_snippet_property/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "code_snippets" ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240103163750_add_deleted_at_code_snippet_property/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "code_snippets" ADD COLUMN "deleted_at" TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | // # Lucia auth 14 | 15 | model User { 16 | id String @id @unique 17 | email String @unique 18 | email_verified Boolean @default(false) 19 | created_at DateTime @default(now()) 20 | updated_at DateTime @updatedAt 21 | 22 | sessions Session[] 23 | keys Key[] 24 | code_snippets CodeSnippet[] 25 | 26 | @@map("users") 27 | } 28 | 29 | model Session { 30 | id String @id @unique 31 | user_id String 32 | active_expires BigInt 33 | idle_expires BigInt 34 | created_at DateTime @default(now()) 35 | updated_at DateTime @updatedAt 36 | 37 | user User @relation(references: [id], fields: [user_id], onDelete: Cascade) 38 | 39 | @@index([user_id]) 40 | @@map("sessions") 41 | } 42 | 43 | model Key { 44 | id String @id @unique 45 | hashed_password String? 46 | user_id String 47 | created_at DateTime @default(now()) 48 | updated_at DateTime @updatedAt 49 | 50 | user User @relation(references: [id], fields: [user_id], onDelete: Cascade) 51 | 52 | @@index([user_id]) 53 | @@map("keys") 54 | } 55 | 56 | // # App 57 | 58 | model CodeSnippet { 59 | id Int @id @default(autoincrement()) 60 | user_id String 61 | name String 62 | code String 63 | is_deleted Boolean @default(false) 64 | created_at DateTime @default(now()) 65 | updated_at DateTime @updatedAt 66 | deleted_at DateTime? 67 | 68 | user User @relation(references: [id], fields: [user_id], onDelete: Cascade) 69 | 70 | @@index([user_id]) 71 | @@map("code_snippets") 72 | } 73 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "schedule": ["before 5am on the first day of the month"], 5 | "prConcurrentLimit": 1, 6 | "separateMajorMinor": false, 7 | "internalChecksFilter": "strict", 8 | "minimumReleaseAge": "3", 9 | "packageRules": [ 10 | { 11 | "matchUpdateTypes": ["major", "minor", "patch"], 12 | "groupName": "all dependencies", 13 | "groupSlug": "update-all-dependencies" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /reports/playwright/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/reports/playwright/.gitkeep -------------------------------------------------------------------------------- /reports/vitest/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/reports/vitest/.gitkeep -------------------------------------------------------------------------------- /scripts/app/lib/lucia/client.js: -------------------------------------------------------------------------------- 1 | // Polyfill the Web Crypto API, required only for Node.js runtime <= version 18 2 | import 'lucia/polyfill/node'; 3 | 4 | import { prisma as prismaAdapter } from '@lucia-auth/adapter-prisma'; 5 | import { lucia } from 'lucia'; 6 | import { node as nodeMiddleware } from 'lucia/middleware'; 7 | 8 | import { prisma as prismaClient } from '../prisma/client.js'; 9 | 10 | export const auth = lucia({ 11 | env: 'DEV', 12 | middleware: nodeMiddleware(), 13 | adapter: prismaAdapter(prismaClient, { 14 | // Values are lowercase names of Prisma models 15 | user: 'user', 16 | session: 'session', 17 | key: 'key', 18 | }), 19 | getUserAttributes: (authUserSchema) => { 20 | return { 21 | email: authUserSchema.email, 22 | email_verified: authUserSchema.email_verified, 23 | }; 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/app/lib/prisma/client.js: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | export const prisma = new PrismaClient(); 4 | -------------------------------------------------------------------------------- /scripts/app/lib/utils/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param envVars {string[]} 3 | * @returns {void} 4 | */ 5 | export function throwIfEnvVarsNotSet(envVars) { 6 | envVars.forEach((envVar) => { 7 | if (!isEnvVarSet(envVar)) { 8 | throw new Error(`Environment variable ${envVar} must be set.`); 9 | } 10 | }); 11 | } 12 | 13 | /** 14 | * @param envVar {string} 15 | * @returns {boolean} 16 | */ 17 | function isEnvVarSet(envVar) { 18 | return ![undefined, ''].includes(process.env[envVar]); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/app/lib/utils/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {(...args: any[]) => Promise | any} callback 3 | */ 4 | export async function logAndIgnoreError(callback) { 5 | try { 6 | await callback(); 7 | } catch (error) { 8 | console.error(error); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/app/seeds/enumeration/deleteAll.js: -------------------------------------------------------------------------------- 1 | import { prisma } from '../../lib/prisma/client.js'; 2 | 3 | /** 4 | * @returns {Promise} 5 | */ 6 | export async function deleteAll() { 7 | console.log(`${deleteAll.name} seed: Deleting all...`); 8 | await deleteData(); 9 | console.log(`${deleteAll.name} seed: Deleted all`); 10 | } 11 | 12 | /** 13 | * @returns {Promise} 14 | */ 15 | async function deleteData() { 16 | await prisma.session.deleteMany(); 17 | await prisma.key.deleteMany(); 18 | await prisma.user.deleteMany(); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/app/seeds/groups/development.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url'; 2 | 3 | import { logAndIgnoreError } from '../../lib/utils/errors.js'; 4 | import { seedCodeSnippets } from '../enumeration/seedCodeSnippets.js'; 5 | import { seedUsersAndSessions } from '../enumeration/seedUsersAndSessions.js'; 6 | 7 | const filepath = fileURLToPath(import.meta.url); 8 | const filename = filepath.split('/').pop()?.split('.')[0]; 9 | 10 | console.log(`${filename} group: Seeding db...`); 11 | await logAndIgnoreError(seedUsersAndSessions); 12 | await logAndIgnoreError(seedCodeSnippets); 13 | console.log(`${filename} group: Seeding complete`); 14 | -------------------------------------------------------------------------------- /scripts/app/seeds/groups/playwright.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url'; 2 | 3 | import { deleteAll } from '../enumeration/deleteAll.js'; 4 | import { seedCodeSnippets } from '../enumeration/seedCodeSnippets.js'; 5 | import { seedUsersAndSessions } from '../enumeration/seedUsersAndSessions.js'; 6 | 7 | const filepath = fileURLToPath(import.meta.url); 8 | const filename = filepath.split('/').pop()?.split('.')[0]; 9 | 10 | console.log(`${filename} group: Seeding db...`); 11 | await deleteAll(); 12 | await seedUsersAndSessions(); 13 | await seedCodeSnippets(); 14 | console.log(`${filename} group: Seeding complete`); 15 | -------------------------------------------------------------------------------- /scripts/docker/images/postgres/exec-psql-connect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | docker container exec \ 6 | --interactive \ 7 | --tty \ 8 | code-snippet-sharing--postgres \ 9 | psql \ 10 | --host localhost \ 11 | --port 5434 \ 12 | --username postgres \ 13 | --dbname postgres 14 | -------------------------------------------------------------------------------- /scripts/northflank/forwarding/postgres-production.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | CURRENT_WORKING_DIRECTORY_ABSOLUTE_PATH=$(realpath .) 6 | SCRIPT_ABSOLUTE_PATH=$(realpath "$0") 7 | SCRIPT_DIRECTORY_ABSOLUTE_PATH=$(dirname "$SCRIPT_ABSOLUTE_PATH") 8 | SCRIPT_RELATIVE_PATH="./$(realpath --relative-to="$CURRENT_WORKING_DIRECTORY_ABSOLUTE_PATH" "$SCRIPT_ABSOLUTE_PATH")" 9 | SCRIPT_DIRECTORY_RELATIVE_PATH="$(dirname "$SCRIPT_RELATIVE_PATH")" 10 | SCRIPT_NAME=$(basename "$SCRIPT_RELATIVE_PATH") 11 | 12 | HELP_DESCRIPTION="\ 13 | NAME 14 | $SCRIPT_NAME 15 | DESCRIPTION 16 | ... 17 | SYNOPSIS 18 | $SCRIPT_NAME 19 | $SCRIPT_NAME -h|--help 20 | OPTIONS 21 | -h|--help Show this help 22 | EXAMPLES 23 | ${SCRIPT_RELATIVE_PATH} 24 | ... 25 | " 26 | 27 | while [ $# -gt 0 ]; do 28 | case $1 in 29 | -h|--help) 30 | echo "$HELP_DESCRIPTION" 31 | exit 0 32 | ;; 33 | *) 34 | echo "ERROR: Unknown option: $1" 35 | echo "" 36 | echo "$HELP_DESCRIPTION" 37 | exit 1 38 | ;; 39 | esac 40 | done 41 | 42 | ENV_FILE=".env.local" 43 | ENV_PATH="${CURRENT_WORKING_DIRECTORY_ABSOLUTE_PATH}/${ENV_FILE}" 44 | $(sed 's/#.*$//g' $ENV_PATH | sed '/^$/d' | sed 's/^/export /g') 45 | 46 | sudo -E "$(which npx)" northflank forward \ 47 | addon \ 48 | --projectId code-snippet-sharing \ 49 | --addonId postgres-production 50 | -------------------------------------------------------------------------------- /scripts/northflank/forwarding/postgres-staging.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | CURRENT_WORKING_DIRECTORY_ABSOLUTE_PATH=$(realpath .) 6 | SCRIPT_ABSOLUTE_PATH=$(realpath "$0") 7 | SCRIPT_DIRECTORY_ABSOLUTE_PATH=$(dirname "$SCRIPT_ABSOLUTE_PATH") 8 | SCRIPT_RELATIVE_PATH="./$(realpath --relative-to="$CURRENT_WORKING_DIRECTORY_ABSOLUTE_PATH" "$SCRIPT_ABSOLUTE_PATH")" 9 | SCRIPT_DIRECTORY_RELATIVE_PATH="$(dirname "$SCRIPT_RELATIVE_PATH")" 10 | SCRIPT_NAME=$(basename "$SCRIPT_RELATIVE_PATH") 11 | 12 | HELP_DESCRIPTION="\ 13 | NAME 14 | $SCRIPT_NAME 15 | DESCRIPTION 16 | ... 17 | SYNOPSIS 18 | $SCRIPT_NAME 19 | $SCRIPT_NAME -h|--help 20 | OPTIONS 21 | -h|--help Show this help 22 | EXAMPLES 23 | ${SCRIPT_RELATIVE_PATH} 24 | ... 25 | " 26 | 27 | while [ $# -gt 0 ]; do 28 | case $1 in 29 | -h|--help) 30 | echo "$HELP_DESCRIPTION" 31 | exit 0 32 | ;; 33 | *) 34 | echo "ERROR: Unknown option: $1" 35 | echo "" 36 | echo "$HELP_DESCRIPTION" 37 | exit 1 38 | ;; 39 | esac 40 | done 41 | 42 | ENV_FILE=".env.local" 43 | ENV_PATH="${CURRENT_WORKING_DIRECTORY_ABSOLUTE_PATH}/${ENV_FILE}" 44 | $(sed 's/#.*$//g' $ENV_PATH | sed '/^$/d' | sed 's/^/export /g') 45 | 46 | sudo -E "$(which npx)" northflank forward \ 47 | addon \ 48 | --projectId code-snippet-sharing \ 49 | --addonId postgres-staging 50 | -------------------------------------------------------------------------------- /scripts/stack/_lib/utils/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | /** 4 | * @param {string} envFileAbsolutePath 5 | * @param {string[]} envVars - Mandatory environment variables to check 6 | */ 7 | export function loadAndCheckEnvVars(envFileAbsolutePath, envVars) { 8 | dotenv.config({ 9 | path: envFileAbsolutePath, 10 | // override: true, 11 | // debug: true, 12 | }); 13 | exitIfEnvVarsNotSet(envVars); 14 | } 15 | 16 | /** 17 | * @param {string[]} envVars 18 | * @returns {void} 19 | */ 20 | export function exitIfEnvVarsNotSet(envVars) { 21 | /** @type {string[]} */ 22 | const notSetEnvVars = []; 23 | envVars.forEach((envVar) => { 24 | if (!isEnvVarSet(envVar)) { 25 | notSetEnvVars.push(envVar); 26 | } 27 | }); 28 | 29 | if (notSetEnvVars.length > 0) { 30 | console.log( 31 | `The following environment variables are not set: ${notSetEnvVars.join( 32 | ', ', 33 | )}`, 34 | ); 35 | process.exit(1); 36 | } 37 | } 38 | 39 | /** 40 | * @param {string} envVar 41 | * @returns {boolean} 42 | */ 43 | function isEnvVarSet(envVar) { 44 | return ![undefined, ''].includes(process.env[envVar]); 45 | } 46 | -------------------------------------------------------------------------------- /scripts/stack/_lib/utils/misc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number} ms 3 | * @returns {Promise} 4 | */ 5 | export function sleep(ms) { 6 | return new Promise((resolve) => setTimeout(resolve, ms)); 7 | } 8 | 9 | /** 10 | * @param {string} url 11 | */ 12 | export async function waitUntilServiceIsAvailable( 13 | url, 14 | maxRetries = 10, 15 | retryIntervalInMs = 1000, 16 | ) { 17 | let retries = 0; 18 | let isResponseOk = false; 19 | while (!isResponseOk) { 20 | try { 21 | const response = await fetch(url); 22 | if (response.ok) { 23 | isResponseOk = true; 24 | } 25 | } catch (error) { 26 | retries += 1; 27 | if (retries >= maxRetries) { 28 | throw new Error( 29 | `Timed out while checking availability of service at ${url}`, 30 | ); 31 | } 32 | await sleep(retryIntervalInMs); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/stack/_lib/utils/paths.js: -------------------------------------------------------------------------------- 1 | import { realpathSync } from 'fs'; 2 | import { basename, dirname } from 'path'; 3 | 4 | /** 5 | * @param {string} scriptPath 6 | * @param {string} envFile 7 | */ 8 | export function getPaths(scriptPath, envFile) { 9 | const currentWorkingDirectoryAbsolutePath = realpathSync('.'); 10 | const scriptAbsolutePath = realpathSync(scriptPath); 11 | const scriptDirectoryAbsolutePath = dirname(scriptAbsolutePath); 12 | const scriptDirectoryParentAbsolutePath = dirname( 13 | scriptDirectoryAbsolutePath, 14 | ); 15 | const scriptRelativePath = `./${scriptAbsolutePath.substring( 16 | currentWorkingDirectoryAbsolutePath.length + 1, 17 | )}`; 18 | const scriptDirectoryRelativePath = dirname(scriptRelativePath); 19 | const scriptDirectoryParentRelativePath = dirname( 20 | scriptDirectoryRelativePath, 21 | ); 22 | const scriptName = basename(scriptRelativePath); 23 | 24 | const envFileAbsolutePath = `${currentWorkingDirectoryAbsolutePath}/${envFile}`; 25 | 26 | return { 27 | currentWorkingDirectoryAbsolutePath, 28 | scriptAbsolutePath, 29 | scriptDirectoryAbsolutePath, 30 | scriptDirectoryParentAbsolutePath, 31 | scriptRelativePath, 32 | scriptDirectoryRelativePath, 33 | scriptDirectoryParentRelativePath, 34 | scriptName, 35 | envFileAbsolutePath, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /scripts/stack/local/all/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const MANDATORY_ENV_VARS = [ 2 | 'PROJECT_NAME', 3 | 'POSTGRES_DATA_VOLUME_NAME', 4 | 'DATABASE_URL', 5 | ]; 6 | -------------------------------------------------------------------------------- /scripts/stack/local/all/api/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ENV_FILE = '.env.playwright'; 2 | export const DOCKER_COMPOSE_FILES = [ 3 | 'docker-compose.local.infra-only.yml', 4 | 'docker-compose.local.app-base.yml', 5 | 'docker-compose.local.app-playwright.yml', 6 | 'docker-compose.local.api.yml', 7 | ]; 8 | -------------------------------------------------------------------------------- /scripts/stack/local/all/api/down.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { stopDockerizedApi } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | stopDockerizedApi(paths, DOCKER_COMPOSE_FILES, true); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/stack/local/all/api/headless.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from 'child_process'; 4 | import { program } from 'commander'; 5 | 6 | import { startDockerizedApi } from '../../../_lib/actions.js'; 7 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 8 | import { getPaths } from '../../../_lib/utils/paths.js'; 9 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 10 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 11 | 12 | main().catch((e) => { 13 | console.error(e); 14 | process.exit(1); 15 | }); 16 | 17 | async function main() { 18 | const scriptPath = /** @type {string} */ (process.argv[1]); 19 | 20 | const paths = getPaths(scriptPath, ENV_FILE); 21 | const { 22 | scriptDirectoryAbsolutePath, 23 | scriptRelativePath, 24 | scriptName, 25 | envFileAbsolutePath, 26 | } = paths; 27 | 28 | program 29 | .name(scriptName) 30 | .description('...') 31 | .option('-l, --leave-stack-up', 'Leave stack up after test run') 32 | .on('option:unknown', (unknownOptionArgs) => { 33 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 34 | process.exit(1); 35 | }) 36 | .addHelpText( 37 | 'after', 38 | ` 39 | Additional options: 40 | -- All options following this option will be sent 41 | to \`docker compose up\` command 42 | 43 | Examples: 44 | ${scriptRelativePath} 45 | ... 46 | `, 47 | ) 48 | .parse(process.argv); 49 | 50 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 51 | const exitCode = await startDockerizedApi(paths, DOCKER_COMPOSE_FILES); 52 | 53 | if (program.opts()['leaveStackUp']) { 54 | console.log( 55 | `Stack is left running. Stop it with \`${scriptDirectoryAbsolutePath}/down.js\``, 56 | ); 57 | process.exit(exitCode); 58 | } 59 | 60 | execSync(`${scriptDirectoryAbsolutePath}/down.js`, { 61 | stdio: 'inherit', 62 | }); 63 | 64 | process.exit(exitCode); 65 | } 66 | -------------------------------------------------------------------------------- /scripts/stack/local/all/e2e/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ENV_FILE = '.env.playwright'; 2 | export const DOCKER_COMPOSE_FILES = [ 3 | 'docker-compose.local.infra-only.yml', 4 | 'docker-compose.local.app-base.yml', 5 | 'docker-compose.local.app-playwright.yml', 6 | 'docker-compose.local.e2e.yml', 7 | ]; 8 | -------------------------------------------------------------------------------- /scripts/stack/local/all/e2e/down.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { stopDockerizedE2E } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | stopDockerizedE2E(paths, DOCKER_COMPOSE_FILES, true); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/stack/local/all/e2e/headless.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from 'child_process'; 4 | import { program } from 'commander'; 5 | 6 | import { startDockerizedE2E } from '../../../_lib/actions.js'; 7 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 8 | import { getPaths } from '../../../_lib/utils/paths.js'; 9 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 10 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 11 | 12 | main().catch((e) => { 13 | console.error(e); 14 | process.exit(1); 15 | }); 16 | 17 | async function main() { 18 | const scriptPath = /** @type {string} */ (process.argv[1]); 19 | 20 | const paths = getPaths(scriptPath, ENV_FILE); 21 | const { 22 | scriptDirectoryAbsolutePath, 23 | scriptRelativePath, 24 | scriptName, 25 | envFileAbsolutePath, 26 | } = paths; 27 | 28 | program 29 | .name(scriptName) 30 | .description('...') 31 | .option('-l, --leave-stack-up', 'Leave stack up after test run') 32 | .on('option:unknown', (unknownOptionArgs) => { 33 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 34 | process.exit(1); 35 | }) 36 | .addHelpText( 37 | 'after', 38 | ` 39 | Additional options: 40 | -- All options following this option will be sent 41 | to \`docker compose up\` command 42 | 43 | Examples: 44 | ${scriptRelativePath} 45 | ... 46 | `, 47 | ) 48 | .parse(process.argv); 49 | 50 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 51 | const exitCode = await startDockerizedE2E(paths, DOCKER_COMPOSE_FILES); 52 | 53 | if (program.opts()['leaveStackUp']) { 54 | console.log( 55 | `Stack is left running. Stop it with \`${scriptDirectoryAbsolutePath}/down.js\``, 56 | ); 57 | process.exit(exitCode); 58 | } 59 | 60 | execSync(`${scriptDirectoryAbsolutePath}/down.js`, { 61 | stdio: 'inherit', 62 | }); 63 | 64 | process.exit(exitCode); 65 | } 66 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const MANDATORY_ENV_VARS = [ 2 | 'PROJECT_NAME', 3 | 'POSTGRES_DATA_VOLUME_NAME', 4 | 'DATABASE_URL', 5 | ]; 6 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/playwright/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ENV_FILE = '.env.playwright'; 2 | export const DOCKER_COMPOSE_FILES = [ 3 | 'docker-compose.local.infra-only.yml', 4 | 'docker-compose.local.app-base.yml', 5 | 'docker-compose.local.app-playwright.yml', 6 | ]; 7 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/playwright/down.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { stopDockerizedInfraApp } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | stopDockerizedInfraApp(paths, DOCKER_COMPOSE_FILES, true); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/stack-only/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ENV_FILE = '.env.local'; 2 | export const DOCKER_COMPOSE_FILES = [ 3 | 'docker-compose.local.infra-only.yml', 4 | 'docker-compose.local.app-base.yml', 5 | 'docker-compose.local.app-only.yml', 6 | ]; 7 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/stack-only/down.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { stopDockerizedInfraApp } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | stopDockerizedInfraApp(paths, DOCKER_COMPOSE_FILES); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/stack-only/infra-app-seed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { 6 | performLocalSeeding, 7 | startDockerizedInfraApp, 8 | } from '../../../_lib/actions.js'; 9 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 10 | import { getPaths } from '../../../_lib/utils/paths.js'; 11 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 12 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 13 | 14 | main().catch((e) => { 15 | console.error(e); 16 | process.exit(1); 17 | }); 18 | 19 | async function main() { 20 | const scriptPath = /** @type {string} */ (process.argv[1]); 21 | 22 | const paths = getPaths(scriptPath, ENV_FILE); 23 | const { 24 | scriptRelativePath, 25 | scriptDirectoryRelativePath, 26 | scriptName, 27 | envFileAbsolutePath, 28 | } = paths; 29 | 30 | program 31 | .name(scriptName) 32 | .description('...') 33 | .on('option:unknown', (unknownOptionArgs) => { 34 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 35 | process.exit(1); 36 | }) 37 | .addHelpText( 38 | 'after', 39 | ` 40 | Additional options: 41 | -- All options following this option will be sent 42 | to \`docker compose up\` command 43 | 44 | Examples: 45 | ${scriptRelativePath} 46 | ... 47 | `, 48 | ) 49 | .parse(process.argv); 50 | 51 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 52 | await startDockerizedInfraApp(paths, DOCKER_COMPOSE_FILES, program.args); 53 | performLocalSeeding(); 54 | 55 | console.log( 56 | `Stack is left running. Stop it with \`${scriptDirectoryRelativePath}/down.js\``, 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /scripts/stack/local/infra-app/stack-only/infra-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { startDockerizedInfraApp } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { DOCKER_COMPOSE_FILES, ENV_FILE } from './_lib/constants.js'; 10 | 11 | main().catch((e) => { 12 | console.error(e); 13 | process.exit(1); 14 | }); 15 | 16 | async function main() { 17 | const scriptPath = /** @type {string} */ (process.argv[1]); 18 | 19 | const paths = getPaths(scriptPath, ENV_FILE); 20 | const { 21 | scriptRelativePath, 22 | scriptDirectoryRelativePath, 23 | scriptName, 24 | envFileAbsolutePath, 25 | } = paths; 26 | 27 | program 28 | .name(scriptName) 29 | .description('...') 30 | .on('option:unknown', (unknownOptionArgs) => { 31 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 32 | process.exit(1); 33 | }) 34 | .addHelpText( 35 | 'after', 36 | ` 37 | Additional options: 38 | -- All options following this option will be sent 39 | to \`docker compose up\` command 40 | 41 | Examples: 42 | ${scriptRelativePath} 43 | ... 44 | `, 45 | ) 46 | .parse(process.argv); 47 | 48 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 49 | await startDockerizedInfraApp(paths, DOCKER_COMPOSE_FILES, program.args); 50 | 51 | console.log( 52 | `Stack is left running. Stop it with \`${scriptDirectoryRelativePath}/down.js\``, 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const MANDATORY_ENV_VARS = [ 2 | 'PROJECT_NAME', 3 | 'POSTGRES_DATA_VOLUME_NAME', 4 | 'DATABASE_URL', 5 | ]; 6 | export const DOCKER_COMPOSE_FILES = ['docker-compose.local.infra-only.yml']; 7 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/playwright/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ENV_FILE = '.env.playwright'; 2 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/playwright/down.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { stopDockerizedInfra, stopLocalApp } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { DOCKER_COMPOSE_FILES, MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | stopLocalApp(); 43 | stopDockerizedInfra(paths, DOCKER_COMPOSE_FILES, true); 44 | } 45 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/playwright/infra-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { 6 | performLocalMigration, 7 | startDockerizedInfra, 8 | startLocalApp, 9 | } from '../../../_lib/actions.js'; 10 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 11 | import { getPaths } from '../../../_lib/utils/paths.js'; 12 | import { DOCKER_COMPOSE_FILES, MANDATORY_ENV_VARS } from '../_lib/constants.js'; 13 | import { ENV_FILE } from './_lib/constants.js'; 14 | 15 | main().catch((e) => { 16 | console.error(e); 17 | process.exit(1); 18 | }); 19 | 20 | async function main() { 21 | const scriptPath = /** @type {string} */ (process.argv[1]); 22 | 23 | const paths = getPaths(scriptPath, ENV_FILE); 24 | const { 25 | scriptRelativePath, 26 | scriptDirectoryRelativePath, 27 | scriptName, 28 | envFileAbsolutePath, 29 | } = paths; 30 | 31 | program 32 | .name(scriptName) 33 | .description('...') 34 | .on('option:unknown', (unknownOptionArgs) => { 35 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 36 | process.exit(1); 37 | }) 38 | .addHelpText( 39 | 'after', 40 | ` 41 | Additional options: 42 | -- All options following this option will be sent 43 | to \`docker compose up\` command 44 | 45 | Examples: 46 | ${scriptRelativePath} 47 | ... 48 | `, 49 | ) 50 | .parse(process.argv); 51 | 52 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 53 | await startDockerizedInfra(paths, DOCKER_COMPOSE_FILES, program.args); 54 | performLocalMigration(); 55 | await startLocalApp(); 56 | 57 | console.log( 58 | `Stack is left running. Stop it with \`${scriptDirectoryRelativePath}/down.js\``, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/_lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ENV_FILE = '.env.local'; 2 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { runLocalApp } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Additional options: 35 | -- All options following this option will be sent 36 | to \`docker compose up\` command 37 | 38 | Examples: 39 | ${scriptRelativePath} 40 | ... 41 | `, 42 | ) 43 | .parse(process.argv); 44 | 45 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 46 | runLocalApp(); 47 | } 48 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/down.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { stopDockerizedInfra, stopLocalApp } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { DOCKER_COMPOSE_FILES, MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | stopLocalApp(); 43 | stopDockerizedInfra(paths, DOCKER_COMPOSE_FILES); 44 | } 45 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/infra-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from 'child_process'; 4 | import { program } from 'commander'; 5 | 6 | import { 7 | performLocalMigration, 8 | runLocalApp, 9 | startDockerizedInfra, 10 | } from '../../../_lib/actions.js'; 11 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 12 | import { getPaths } from '../../../_lib/utils/paths.js'; 13 | import { DOCKER_COMPOSE_FILES, MANDATORY_ENV_VARS } from '../_lib/constants.js'; 14 | import { ENV_FILE } from './_lib/constants.js'; 15 | 16 | main().catch((e) => { 17 | console.error(e); 18 | process.exit(1); 19 | }); 20 | 21 | async function main() { 22 | const scriptPath = /** @type {string} */ (process.argv[1]); 23 | 24 | const paths = getPaths(scriptPath, ENV_FILE); 25 | const { 26 | scriptRelativePath, 27 | scriptDirectoryRelativePath, 28 | scriptName, 29 | envFileAbsolutePath, 30 | } = paths; 31 | 32 | program 33 | .name(scriptName) 34 | .description('...') 35 | .on('option:unknown', (unknownOptionArgs) => { 36 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 37 | process.exit(1); 38 | }) 39 | .addHelpText( 40 | 'after', 41 | ` 42 | Additional options: 43 | -- All options following this option will be sent 44 | to \`docker compose up\` command 45 | 46 | Examples: 47 | ${scriptRelativePath} 48 | ... 49 | `, 50 | ) 51 | .parse(process.argv); 52 | 53 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 54 | await startDockerizedInfra(paths, DOCKER_COMPOSE_FILES, program.args); 55 | performLocalMigration(); 56 | runLocalApp(); 57 | 58 | execSync(`${scriptDirectoryRelativePath}/down.js`, { 59 | stdio: 'inherit', 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/infra-migrate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { 6 | performLocalMigration, 7 | startDockerizedInfra, 8 | } from '../../../_lib/actions.js'; 9 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 10 | import { getPaths } from '../../../_lib/utils/paths.js'; 11 | import { DOCKER_COMPOSE_FILES, MANDATORY_ENV_VARS } from '../_lib/constants.js'; 12 | import { ENV_FILE } from './_lib/constants.js'; 13 | 14 | main().catch((e) => { 15 | console.error(e); 16 | process.exit(1); 17 | }); 18 | 19 | async function main() { 20 | const scriptPath = /** @type {string} */ (process.argv[1]); 21 | 22 | const paths = getPaths(scriptPath, ENV_FILE); 23 | const { 24 | scriptRelativePath, 25 | scriptDirectoryRelativePath, 26 | scriptName, 27 | envFileAbsolutePath, 28 | } = paths; 29 | 30 | program 31 | .name(scriptName) 32 | .description('...') 33 | .on('option:unknown', (unknownOptionArgs) => { 34 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 35 | process.exit(1); 36 | }) 37 | .addHelpText( 38 | 'after', 39 | ` 40 | Additional options: 41 | -- All options following this option will be sent 42 | to \`docker compose up\` command 43 | 44 | Examples: 45 | ${scriptRelativePath} 46 | ... 47 | `, 48 | ) 49 | .parse(process.argv); 50 | 51 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 52 | await startDockerizedInfra(paths, DOCKER_COMPOSE_FILES, program.args); 53 | performLocalMigration(); 54 | 55 | console.log( 56 | `Stack is left running. Stop it with \`${scriptDirectoryRelativePath}/down.js\``, 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/infra.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { startDockerizedInfra } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { DOCKER_COMPOSE_FILES, MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { ENV_FILE } from './_lib/constants.js'; 10 | 11 | main().catch((e) => { 12 | console.error(e); 13 | process.exit(1); 14 | }); 15 | 16 | async function main() { 17 | const scriptPath = /** @type {string} */ (process.argv[1]); 18 | 19 | const paths = getPaths(scriptPath, ENV_FILE); 20 | const { 21 | scriptRelativePath, 22 | scriptDirectoryRelativePath, 23 | scriptName, 24 | envFileAbsolutePath, 25 | } = paths; 26 | 27 | program 28 | .name(scriptName) 29 | .description('...') 30 | .on('option:unknown', (unknownOptionArgs) => { 31 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 32 | process.exit(1); 33 | }) 34 | .addHelpText( 35 | 'after', 36 | ` 37 | Additional options: 38 | -- All options following this option will be sent 39 | to \`docker compose up\` command 40 | 41 | Examples: 42 | ${scriptRelativePath} 43 | ... 44 | `, 45 | ) 46 | .parse(process.argv); 47 | 48 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 49 | await startDockerizedInfra(paths, DOCKER_COMPOSE_FILES, program.args); 50 | 51 | console.log( 52 | `Stack is left running. Stop it with \`${scriptDirectoryRelativePath}/down.js\``, 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/migrate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { performLocalMigration } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Additional options: 35 | -- All options following this option will be sent 36 | to \`prisma migrate dev\` command 37 | 38 | Examples: 39 | ${scriptRelativePath} 40 | ... 41 | `, 42 | ) 43 | .parse(process.argv); 44 | 45 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 46 | performLocalMigration(program.args.join(' ')); 47 | } 48 | -------------------------------------------------------------------------------- /scripts/stack/local/infra/stack-only/seed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | 5 | import { performLocalSeeding } from '../../../_lib/actions.js'; 6 | import { loadAndCheckEnvVars } from '../../../_lib/utils/env.js'; 7 | import { getPaths } from '../../../_lib/utils/paths.js'; 8 | import { MANDATORY_ENV_VARS } from '../_lib/constants.js'; 9 | import { ENV_FILE } from './_lib/constants.js'; 10 | 11 | try { 12 | main(); 13 | } catch (e) { 14 | console.error(e); 15 | process.exit(1); 16 | } 17 | 18 | function main() { 19 | const scriptPath = /** @type {string} */ (process.argv[1]); 20 | 21 | const paths = getPaths(scriptPath, ENV_FILE); 22 | const { scriptRelativePath, scriptName, envFileAbsolutePath } = paths; 23 | 24 | program 25 | .name(scriptName) 26 | .description('...') 27 | .on('option:unknown', (unknownOptionArgs) => { 28 | console.error(`Unknown option: ${unknownOptionArgs[0]}`); 29 | process.exit(1); 30 | }) 31 | .addHelpText( 32 | 'after', 33 | ` 34 | Examples: 35 | ${scriptRelativePath} 36 | ... 37 | `, 38 | ) 39 | .parse(process.argv); 40 | 41 | loadAndCheckEnvVars(envFileAbsolutePath, MANDATORY_ENV_VARS); 42 | performLocalSeeding(); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/testing/load/commands/run-sh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | docker container run \ 6 | --name "graphana-k6" \ 7 | --interactive \ 8 | --tty \ 9 | --rm \ 10 | --user "k6" \ 11 | --workdir "/home/k6" \ 12 | --volume "${PWD}:/home/k6" \ 13 | --entrypoint "/bin/sh" \ 14 | "grafana/k6:0.49.0" 15 | -------------------------------------------------------------------------------- /scripts/testing/load/commands/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | docker container run \ 6 | --name "graphana-k6" \ 7 | --interactive \ 8 | --tty \ 9 | --rm \ 10 | --user "root" \ 11 | --workdir "/home/k6" \ 12 | --volume "${PWD}:/home/k6" \ 13 | "grafana/k6:0.49.0" \ 14 | $@ 15 | -------------------------------------------------------------------------------- /scripts/testing/load/tests/load-1.load-test.js: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'; 3 | // @ts-expect-error 4 | import { check, sleep } from 'k6'; 5 | // @ts-expect-error 6 | import http from 'k6/http'; 7 | // @ts-expect-error 8 | import { Rate } from 'k6/metrics'; 9 | 10 | // Define the failure rate 11 | let failureRate = new Rate('failed_requests'); 12 | 13 | export let options = { 14 | stages: [ 15 | { duration: '10s', target: 10 }, // ramp up to 5 requests per second over 10 seconds 16 | { duration: '10s', target: 10 }, // stay at 5 requests per second for 10 seconds 17 | { duration: '10s', target: 1 }, // ramp down to 1 request per second over 10 seconds 18 | ], 19 | thresholds: { 20 | 'http_req_duration{scenario:default}': ['p(95)<5000'], // Test fails if 95th percentile of request durations is higher than 5 seconds 21 | failed_requests: ['rate<0.01'], // Fail if more than 1% of requests fail 22 | }, 23 | }; 24 | 25 | export default function () { 26 | let response = http.get('http://staging-code-snippet-sharing.nodeexx.com'); 27 | 28 | // Check each response for a 200 status code 29 | const checkRes = check(response, { 30 | // @ts-expect-error 31 | 'status is 200': (r) => r.status === 200, 32 | }); 33 | 34 | // Track the failed requests 35 | failureRate.add(!checkRes); 36 | 37 | // Since the test doesn't precisely control the request rate to exactly match the target, 38 | // sleeping for a short duration helps to regulate the execution pace. 39 | sleep(1); 40 | } 41 | 42 | // @ts-expect-error 43 | export function handleSummary(data) { 44 | return { 45 | 'summary.html': htmlReport(data), 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | import 'unplugin-icons/types/svelte'; 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | declare global { 6 | namespace Lucia { 7 | type Auth = import('$lib/server/lucia/types').Auth; 8 | // Database user = AuthUserSchema 9 | // Properties of the auth user schema that Lucia can be aware of 10 | interface DatabaseUserAttributes { 11 | email: string; 12 | email_verified: boolean; 13 | created_at?: Date; 14 | } 15 | type DatabaseSessionAttributes = Record; 16 | } 17 | 18 | namespace App { 19 | // interface Error {} 20 | 21 | interface Locals { 22 | authRequest: import('lucia').AuthRequest; 23 | authSession: import('$lib/shared/lucia/types').AuthSession | null; 24 | authUser: import('$lib/shared/lucia/types').AuthUser | null; 25 | } 26 | 27 | interface PageData { 28 | flash?: GlobalMessage; 29 | authUser: import('$lib/shared/lucia/types').AuthUser | null; 30 | } 31 | 32 | // interface Platform {} 33 | 34 | namespace Superforms { 35 | type Message = GlobalMessage; 36 | } 37 | 38 | type GlobalMessageType = 'error' | 'success'; 39 | interface GlobalMessage { 40 | type: GlobalMessageType; 41 | message: string; 42 | } 43 | } 44 | } 45 | 46 | export {}; 47 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
11 | %sveltekit.body% 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app.postcss: -------------------------------------------------------------------------------- 1 | /** 2 | * Write your global styles here, in PostCSS syntax 3 | */ 4 | /* https://tailwindcss.com/docs/using-with-preprocessors#build-time-imports */ 5 | 6 | /* @tailwind base; */ 7 | @import 'tailwindcss/base'; 8 | @import './styles/base.postcss'; 9 | 10 | /* @tailwind components; */ 11 | @import 'tailwindcss/components'; 12 | 13 | /* @tailwind utilities; */ 14 | @import 'tailwindcss/utilities'; 15 | 16 | /* @tailwind variants; */ 17 | @import 'tailwindcss/variants'; 18 | -------------------------------------------------------------------------------- /src/hooks.client.ts: -------------------------------------------------------------------------------- 1 | import type { HandleClientError } from '@sveltejs/kit'; 2 | 3 | import { dev } from '$app/environment'; 4 | import { config } from '$lib/client/core/config'; 5 | import { logger } from '$lib/client/logging'; 6 | import { setupBrowserPosthogClient } from '$lib/client/posthog'; 7 | import { 8 | getClientSentryIntegrations, 9 | setClientPosthogSessionId, 10 | } from '$lib/client/sentry/utils'; 11 | import { handleErrorWithSentry, setupSentryClient } from '$lib/shared/sentry'; 12 | 13 | setupBrowserPosthogClient(config.posthog.projectApiKey, config.posthog.apiHost); 14 | setupSentryClient({ 15 | dsn: config.sentry.dsn, 16 | environment: config.sentry.environment, 17 | // SvelteKit's page store is empty at this point 18 | origin: window.location.origin, 19 | integrations: [ 20 | ...getClientSentryIntegrations( 21 | config.sentry.organization, 22 | config.sentry.projectId, 23 | ), 24 | ], 25 | }); 26 | setClientPosthogSessionId(); 27 | 28 | logger.info('Starting the app client...'); 29 | 30 | export const handleError = handleErrorWithSentry((({ error }) => { 31 | const message = 'Internal Client Error'; 32 | if (dev) { 33 | console.error(message, error); 34 | } 35 | 36 | return { 37 | message, 38 | status: 500, 39 | }; 40 | }) satisfies HandleClientError); 41 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/AppBar.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/AppBar.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import Component from './AppBar.svelte'; 5 | 6 | describe(Component.name, () => { 7 | afterEach(() => { 8 | cleanup(); 9 | }); 10 | 11 | it('should render', () => { 12 | const renderResult = render(Component); 13 | 14 | expect(renderResult.component).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/AppMenuButton.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if $page.data.authUser} 19 | 20 | 26 | 27 | {:else} 28 | 29 | 32 | 33 | {/if} 34 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/AppShell.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | {#if appBar} 12 | 13 | {/if} 14 | 15 | 16 |
17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/AppShell.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import { SlotTest } from '../testing'; 5 | import Component from './AppShell.svelte'; 6 | 7 | describe(Component.name, () => { 8 | afterEach(() => { 9 | cleanup(); 10 | }); 11 | 12 | it('should render with navigation bar', () => { 13 | const renderResult = render(Component); 14 | 15 | expect(renderResult.queryByTestId('app-bar')).toBeTruthy(); 16 | }); 17 | 18 | it('should render without navigation bar', () => { 19 | const renderResult = render(Component, { 20 | props: { appBar: false }, 21 | }); 22 | 23 | expect(renderResult.queryByTestId('app-bar')).toBeFalsy(); 24 | }); 25 | 26 | it('should render slot contents', () => { 27 | const renderResult = render(SlotTest, { props: { component: Component } }); 28 | 29 | expect(renderResult.queryByText('mock-slot-text')).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/Error.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 |
22 |

{title}

23 |

{message}

24 |
25 |
26 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/Error.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { 3 | afterEach, 4 | beforeEach, 5 | describe, 6 | expect, 7 | it, 8 | type MockInstance, 9 | vi, 10 | } from 'vitest'; 11 | 12 | import * as appNavigationModule from '$app/navigation'; 13 | 14 | import Component from './Error.svelte'; 15 | 16 | describe(Component.name, () => { 17 | let invalidateAllSpy: MockInstance; 18 | 19 | beforeEach(() => { 20 | invalidateAllSpy = vi.spyOn(appNavigationModule, 'invalidateAll'); 21 | }); 22 | 23 | afterEach(() => { 24 | cleanup(); 25 | vi.restoreAllMocks(); 26 | }); 27 | 28 | it('should show navigation bar and call invalidateAll', () => { 29 | const renderResult = render(Component); 30 | 31 | expect(renderResult.queryByTestId('app-bar')).toBeTruthy(); 32 | expect(invalidateAllSpy).toHaveBeenCalled(); 33 | }); 34 | 35 | it('should not show navigation bar and not call invalidateAll', () => { 36 | const renderResult = render(Component, { 37 | showAppBar: false, 38 | }); 39 | 40 | expect(renderResult.queryByTestId('app-bar')).toBeFalsy(); 41 | expect(invalidateAllSpy).not.toHaveBeenCalled(); 42 | }); 43 | 44 | it('should show custom text and message', () => { 45 | const title = 'Test Title'; 46 | const message = 'Test Message'; 47 | const renderResult = render(Component, { 48 | title, 49 | message, 50 | }); 51 | 52 | expect(renderResult.getByText(title)).toBeTruthy(); 53 | expect(renderResult.getByText(message)).toBeTruthy(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/PageMessage.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/PageMessage.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it, vi } from 'vitest'; 3 | 4 | import Component from './PageMessage.svelte'; 5 | 6 | describe(Component.name, () => { 7 | afterEach(() => { 8 | cleanup(); 9 | vi.restoreAllMocks(); 10 | }); 11 | 12 | it('should render the error variant', () => { 13 | const renderResult = render(Component, { 14 | props: { 15 | type: 'error', 16 | message: 'Error message', 17 | }, 18 | }); 19 | 20 | expect(renderResult.queryByText('Error message')).toBeTruthy(); 21 | expect(renderResult.queryByTestId('error-page-message')).toBeTruthy(); 22 | expect(renderResult.queryByTestId('success-page-message')).toBeFalsy(); 23 | }); 24 | 25 | it('should render the success variant', () => { 26 | const renderResult = render(Component, { 27 | props: { 28 | type: 'success', 29 | message: 'Success message', 30 | }, 31 | }); 32 | 33 | expect(renderResult.queryByText('Success message')).toBeTruthy(); 34 | expect(renderResult.queryByTestId('error-page-message')).toBeFalsy(); 35 | expect(renderResult.queryByTestId('success-page-message')).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/lib/client/components/app-shell/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppBar } from './AppBar.svelte'; 2 | export { default as AppMenuButton } from './AppMenuButton.svelte'; 3 | export { default as AppShell } from './AppShell.svelte'; 4 | export { default as Error } from './Error.svelte'; 5 | export { default as PageMessage } from './PageMessage.svelte'; 6 | -------------------------------------------------------------------------------- /src/lib/client/components/code-snippets/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CodeSnippetCard } from './CodeSnippetCard.svelte'; 2 | export { default as CodeSnippetCreateEditForm } from './CodeSnippetCreateEditForm.svelte'; 3 | -------------------------------------------------------------------------------- /src/lib/client/components/common/Alert.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | 55 | -------------------------------------------------------------------------------- /src/lib/client/components/common/Alert.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it, vi } from 'vitest'; 3 | 4 | import Component from './Alert.svelte'; 5 | 6 | describe(Component.name, () => { 7 | afterEach(() => { 8 | cleanup(); 9 | vi.restoreAllMocks(); 10 | }); 11 | 12 | it('should render the error variant', () => { 13 | const renderResult = render(Component, { 14 | props: { 15 | type: 'error', 16 | message: 'Error message', 17 | }, 18 | }); 19 | 20 | expect(renderResult.queryByText('Error message')).toBeTruthy(); 21 | expect(renderResult.queryByTestId('error-alert')).toBeTruthy(); 22 | expect(renderResult.queryByTestId('success-alert')).toBeFalsy(); 23 | }); 24 | 25 | it('should render the success variant', () => { 26 | const renderResult = render(Component, { 27 | props: { 28 | type: 'success', 29 | message: 'Success message', 30 | }, 31 | }); 32 | 33 | expect(renderResult.queryByText('Success message')).toBeTruthy(); 34 | expect(renderResult.queryByTestId('error-alert')).toBeFalsy(); 35 | expect(renderResult.queryByTestId('success-alert')).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/lib/client/components/common/Card.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 | {#if $$slots.header} 23 |
24 | 25 |
26 | {/if} 27 | 28 | {#if $$slots.default} 29 |
30 | 31 |
32 | {/if} 33 | 34 | {#if $$slots.footer} 35 |
36 | 37 |
38 | {/if} 39 |
40 | -------------------------------------------------------------------------------- /src/lib/client/components/common/Card.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanup, 3 | queries, 4 | render, 5 | type RenderResult, 6 | } from '@testing-library/svelte'; 7 | import { afterEach, beforeEach, describe, expect, it } from 'vitest'; 8 | 9 | import { SlotTest } from '../testing'; 10 | import Component from './Card.svelte'; 11 | 12 | describe(Component.name, () => { 13 | let renderResult: RenderResult; 14 | 15 | beforeEach(() => { 16 | renderResult = render(Component); 17 | }); 18 | 19 | afterEach(() => { 20 | cleanup(); 21 | }); 22 | 23 | it('should render the component', () => { 24 | expect(renderResult.component).toBeTruthy(); 25 | }); 26 | 27 | it('should render default slot contents', () => { 28 | const renderResult = render(SlotTest, { props: { component: Component } }); 29 | 30 | expect(renderResult.queryByText('mock-slot-text')).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/lib/client/components/common/SimplePaginator.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if previousPageUrlPath || nextPageUrlPath} 10 |
11 | {#if previousPageUrlPath} 12 | 13 | 21 | 22 | {/if} 23 | {#if nextPageUrlPath} 24 | 25 | 33 | 34 | {/if} 35 |
36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/client/components/common/SimplePaginator.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import Component from './SimplePaginator.svelte'; 5 | 6 | describe(Component.name, () => { 7 | afterEach(() => { 8 | cleanup(); 9 | }); 10 | 11 | it('should render no content', () => { 12 | const renderResult = render(Component); 13 | 14 | expect(renderResult.queryByTestId('simple-paginator')).toBeFalsy(); 15 | }); 16 | 17 | it('should render only the previous button', () => { 18 | const renderResult = render(Component, { 19 | props: { 20 | previousPageUrlPath: '/?page=1', 21 | nextPageUrlPath: undefined, 22 | }, 23 | }); 24 | 25 | expect(renderResult.queryByTestId('simple-paginator')).toBeTruthy(); 26 | expect(renderResult.queryByTestId('previous-button')).toBeTruthy(); 27 | expect(renderResult.queryByTestId('next-button')).toBeFalsy(); 28 | }); 29 | 30 | it('should render only the next button', () => { 31 | const renderResult = render(Component, { 32 | props: { 33 | previousPageUrlPath: undefined, 34 | nextPageUrlPath: '/?page=2', 35 | }, 36 | }); 37 | 38 | expect(renderResult.queryByTestId('simple-paginator')).toBeTruthy(); 39 | expect(renderResult.queryByTestId('previous-button')).toBeFalsy(); 40 | expect(renderResult.queryByTestId('next-button')).toBeTruthy(); 41 | }); 42 | 43 | it('should render both buttons', () => { 44 | const renderResult = render(Component, { 45 | props: { 46 | previousPageUrlPath: '/', 47 | nextPageUrlPath: '/?page=3', 48 | }, 49 | }); 50 | 51 | expect(renderResult.queryByTestId('simple-paginator')).toBeTruthy(); 52 | expect(renderResult.queryByTestId('previous-button')).toBeTruthy(); 53 | expect(renderResult.queryByTestId('next-button')).toBeTruthy(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/lib/client/components/common/SingleCardPageContainer.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/lib/client/components/common/SingleCardPageContainer.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import { SlotTest } from '../testing'; 5 | import Component from './SingleCardPageContainer.svelte'; 6 | 7 | describe(Component.name, () => { 8 | afterEach(() => { 9 | cleanup(); 10 | }); 11 | 12 | it('should render the component', () => { 13 | const renderResult = render(Component); 14 | expect(renderResult.component).toBeTruthy(); 15 | }); 16 | 17 | it('should render default slot contents', () => { 18 | const renderResult = render(SlotTest, { props: { component: Component } }); 19 | 20 | expect(renderResult.queryByText('mock-slot-text')).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/lib/client/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Alert } from './Alert.svelte'; 2 | export { default as Card } from './Card.svelte'; 3 | export { default as SimplePaginator } from './SimplePaginator.svelte'; 4 | export { default as SingleCardPageContainer } from './SingleCardPageContainer.svelte'; 5 | -------------------------------------------------------------------------------- /src/lib/client/components/testing/SlotTest.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
{text}
8 |
9 | -------------------------------------------------------------------------------- /src/lib/client/components/testing/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SlotTest } from './SlotTest.svelte'; 2 | -------------------------------------------------------------------------------- /src/lib/client/core/config/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PUBLIC_POSTHOG_API_HOST, 3 | PUBLIC_POSTHOG_PROJECT_API_KEY, 4 | PUBLIC_SENTRY_DSN, 5 | PUBLIC_SENTRY_ENVIRONMENT, 6 | PUBLIC_SENTRY_ORGANIZATION, 7 | PUBLIC_SENTRY_PROJECT_ID, 8 | } from '$env/static/public'; 9 | 10 | export const config = { 11 | appName: 'Code Snippet Sharing app', 12 | get pageTitleSuffix() { 13 | return ` | ${this.appName}`; 14 | }, 15 | posthog: { 16 | projectApiKey: PUBLIC_POSTHOG_PROJECT_API_KEY, 17 | apiHost: PUBLIC_POSTHOG_API_HOST, 18 | }, 19 | sentry: { 20 | dsn: PUBLIC_SENTRY_DSN, 21 | environment: PUBLIC_SENTRY_ENVIRONMENT, 22 | organization: PUBLIC_SENTRY_ORGANIZATION, 23 | projectId: getSentryProjectId(), 24 | }, 25 | logger: { 26 | get minLogLevel(): string { 27 | return globalThis.localStorage.getItem('LOGGER_MIN_LOG_LEVEL') ?? 'info'; 28 | }, 29 | get isDebugContextShown(): boolean { 30 | return ( 31 | globalThis.localStorage.getItem('LOGGER_SHOW_DEBUG_CONTEXT') === 'true' 32 | ); 33 | }, 34 | }, 35 | }; 36 | 37 | function getSentryProjectId(): number | undefined { 38 | const projectId = Number(PUBLIC_SENTRY_PROJECT_ID); 39 | 40 | return isNaN(projectId) ? undefined : projectId; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/client/core/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './previous-app-page.store'; 2 | -------------------------------------------------------------------------------- /src/lib/client/core/stores/previous-app-page.store.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationTarget } from '@sveltejs/kit'; 2 | import { 3 | type Invalidator, 4 | type Subscriber, 5 | type Unsubscriber, 6 | writable, 7 | } from 'svelte/store'; 8 | 9 | import { navigating } from '$app/stores'; 10 | 11 | export function _createPreviousAppPageStore() { 12 | const previousAppPage = writable(); 13 | 14 | function subscribe( 15 | run: Subscriber, 16 | invalidate?: Invalidator, 17 | ): Unsubscriber { 18 | const unsubscribePreviousAppPage = previousAppPage.subscribe( 19 | run, 20 | invalidate, 21 | ); 22 | 23 | const unsubscribeNavigating = navigating.subscribe(($navigating) => { 24 | if (!$navigating) { 25 | // After navigation 26 | return; 27 | } 28 | 29 | // Before navigation 30 | previousAppPage.set($navigating.from ?? undefined); 31 | }); 32 | 33 | return () => { 34 | unsubscribeNavigating(); 35 | unsubscribePreviousAppPage(); 36 | }; 37 | } 38 | 39 | return { subscribe }; 40 | } 41 | 42 | export const previousAppPage = _createPreviousAppPageStore(); 43 | -------------------------------------------------------------------------------- /src/lib/client/core/stores/testing/index.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationTarget } from '@sveltejs/kit'; 2 | 3 | export const mockPreviousAppPageValue: NavigationTarget = { 4 | params: {}, 5 | route: { 6 | id: '/some/path', 7 | }, 8 | url: new URL('http://localhost/some/path'), 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/client/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './linting.utils'; 2 | export * from './navigation.utils'; 3 | -------------------------------------------------------------------------------- /src/lib/client/core/utils/linting.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to avoid the following lint errors: 3 | * - ESLint `svelte/valid-compile` 4 | * - Svelte `unused-export-let` 5 | */ 6 | export function ignoreUnusedExportLetLintErrors(..._args: any[]) {} 7 | -------------------------------------------------------------------------------- /src/lib/client/core/utils/navigation.utils.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'svelte/store'; 2 | 3 | import { goto } from '$app/navigation'; 4 | import { page } from '$app/stores'; 5 | 6 | import { previousAppPage } from '../stores/previous-app-page.store'; 7 | 8 | export async function goBack() { 9 | const previousAppPageUrl = get(previousAppPage)?.url; 10 | if (previousAppPageUrl) { 11 | await goto(previousAppPageUrl); 12 | return; 13 | } 14 | 15 | const currentOrigin = get(page).url.origin; 16 | const referrer = document.referrer; 17 | if (referrer.startsWith(currentOrigin) || referrer.startsWith('/')) { 18 | await goto(referrer); 19 | return; 20 | } 21 | 22 | await goto('/'); 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/client/global-messages/types/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * NOTE: Those types should be used only in `*.svelte` files, because those do 3 | * not support `global` types (e.g. from `app.d.ts`). 4 | */ 5 | export type GlobalMessage = App.GlobalMessage; 6 | export type GlobalMessageType = App.GlobalMessageType; 7 | -------------------------------------------------------------------------------- /src/lib/client/logging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/lib/client/logging/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { logLevels } from '../utils'; 2 | 3 | export type LogLevelName = keyof typeof logLevels; 4 | -------------------------------------------------------------------------------- /src/lib/client/logging/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { posthog } from '$lib/client/posthog'; 2 | import { getSessionId } from '$lib/client/posthog/utils'; 3 | import type { LoggerContext } from '$lib/shared/logging/types'; 4 | 5 | import type { LogLevelName } from '../types'; 6 | 7 | export const logLevels = { 8 | debug: 10, 9 | log: 20, 10 | info: 30, 11 | warn: 40, 12 | error: 50, 13 | } as const; 14 | 15 | /* 16 | * Colors copied from https://github.com/gajus/roarr-browser-log-writer 17 | */ 18 | export const logLevelColors = { 19 | debug: { 20 | backgroundColor: '#666', 21 | color: '#fff', 22 | }, 23 | error: { 24 | backgroundColor: '#f05033', 25 | color: '#fff', 26 | }, 27 | info: { 28 | backgroundColor: '#3174f1', 29 | color: '#fff', 30 | }, 31 | log: { 32 | backgroundColor: '#712bde', 33 | color: '#fff', 34 | }, 35 | warn: { 36 | backgroundColor: '#f5a623', 37 | color: '#000', 38 | }, 39 | } satisfies { 40 | [key in LogLevelName]: { backgroundColor: string; color: string }; 41 | }; 42 | 43 | /* 44 | * Colors copied from https://github.com/gajus/roarr-browser-log-writer 45 | */ 46 | export const logTimestampColors = { 47 | debug: { 48 | color: '#999', 49 | }, 50 | error: { 51 | color: '#ff1a1a', 52 | }, 53 | info: { 54 | color: '#3291ff', 55 | }, 56 | log: { 57 | color: '#8367d3', 58 | }, 59 | warn: { 60 | color: '#f7b955', 61 | }, 62 | } satisfies { 63 | [key in LogLevelName]: { color: string }; 64 | }; 65 | 66 | export function enrichLoggerContextWithPosthogSessionId( 67 | context: LoggerContext, 68 | ): LoggerContext { 69 | if (!posthog) { 70 | return { ...context }; 71 | } 72 | 73 | const sessionId = getSessionId(); 74 | if (!sessionId) { 75 | return { ...context }; 76 | } 77 | 78 | return { 79 | ...context, 80 | posthogSessionId: getSessionId(), 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/client/posthog/client.ts: -------------------------------------------------------------------------------- 1 | import type { PostHog } from 'posthog-js'; 2 | import _posthog from 'posthog-js'; 3 | 4 | import { _setupPosthogClientBase } from '$lib/shared/posthog/utils'; 5 | 6 | export let posthog: PostHog | undefined; 7 | 8 | export function setupBrowserPosthogClient( 9 | projectApiKey: string | undefined, 10 | apiHost: string | undefined, 11 | ): PostHog | undefined { 12 | posthog = _setupPosthogClientBase( 13 | projectApiKey, 14 | apiHost, 15 | posthog, 16 | getBrowserPosthogClient, 17 | ); 18 | 19 | return posthog; 20 | } 21 | 22 | function getBrowserPosthogClient( 23 | projectApiKey: string, 24 | apiHost: string, 25 | ): PostHog { 26 | // NOTE: `init` does not validate project API key and API host 27 | // at all, invalid value cause problems later when Posthog client tries 28 | // to send events to the API. 29 | _posthog.init(projectApiKey, { 30 | api_host: apiHost, 31 | persistence: 'memory', 32 | autocapture: true, 33 | capture_pageview: false, 34 | capture_pageleave: false, 35 | }); 36 | 37 | return _posthog; 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/client/posthog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './posthog-default-page-events-capture.configurator'; 3 | export * from './posthog-user-identity.configurator'; 4 | -------------------------------------------------------------------------------- /src/lib/client/posthog/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { posthog } from '../client'; 2 | 3 | export function getSessionId(): string | undefined { 4 | return posthog?.get_session_id(); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/client/sentry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sentry-user-identity.configurator'; 2 | -------------------------------------------------------------------------------- /src/lib/client/sentry/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { Integration } from '@sentry/types'; 2 | 3 | import { posthog } from '$lib/client/posthog'; 4 | import { getSessionId } from '$lib/client/posthog/utils'; 5 | import { sentry } from '$lib/shared/sentry'; 6 | 7 | const POSTHOG_SESSION_ID_TAG = 'posthog_session_id'; 8 | 9 | export function getClientSentryIntegrations( 10 | organization: string | undefined, 11 | projectId: number | undefined, 12 | ): Integration[] { 13 | if (posthog && organization && projectId != null) { 14 | return [new posthog.SentryIntegration(posthog, organization, projectId)]; 15 | } 16 | 17 | return []; 18 | } 19 | 20 | export function setClientPosthogSessionId(): void { 21 | if (posthog) { 22 | sentry?.setTag(POSTHOG_SESSION_ID_TAG, getSessionId()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/client/skeleton/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './setup.utils'; 2 | -------------------------------------------------------------------------------- /src/lib/client/skeleton/utils/setup.utils.ts: -------------------------------------------------------------------------------- 1 | // Skeleton Popup required middleware 2 | import { 3 | arrow, 4 | autoUpdate, 5 | computePosition, 6 | flip, 7 | type Middleware, 8 | offset, 9 | shift, 10 | } from '@floating-ui/dom'; 11 | // Skeleton Popup optional middleware 12 | import { autoPlacement, hide, inline, size } from '@floating-ui/dom'; 13 | import { initializeStores, storePopup } from '@skeletonlabs/skeleton'; 14 | 15 | type SkeletonPopupMiddlewareFactory = () => Middleware; 16 | 17 | export function setupSkeletonPopup() { 18 | let floatingUiDomOptionalMiddleware: SkeletonPopupMiddlewareFactory[] = []; 19 | floatingUiDomOptionalMiddleware = [size, autoPlacement, hide, inline]; 20 | 21 | // For Skeleton Popup 22 | storePopup.set({ 23 | computePosition, 24 | autoUpdate, 25 | offset, 26 | // @ts-expect-error 27 | shift, 28 | flip, 29 | arrow, 30 | ...floatingUiDomOptionalMiddleware, 31 | }); 32 | } 33 | 34 | export function setupSkeletonModalToastDrawer(): void { 35 | initializeStores(); 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/client/superforms/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface SuperformsOnErrorResult { 2 | type: 'error'; 3 | status?: number; 4 | error: App.Error; 5 | } 6 | 7 | export type SuperformsMessage = App.Superforms.Message; 8 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/form-actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code-snippets.form-actions'; 2 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './singletons'; 2 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code-snippets.service'; 2 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/singletons.ts: -------------------------------------------------------------------------------- 1 | import { CodeSnippetsService } from './services/code-snippets.service'; 2 | 3 | export const codeSnippetsService = new CodeSnippetsService(); 4 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/utils/errors.node-test.ts: -------------------------------------------------------------------------------- 1 | import * as sveltejsKitModule from '@sveltejs/kit'; 2 | import { describe, expect, it, vi } from 'vitest'; 3 | 4 | import { throwCodeSnippetNotFoundError } from './errors'; 5 | 6 | describe(throwCodeSnippetNotFoundError.name, () => { 7 | it('should throw a 404 error', () => { 8 | const errorSpy = vi.spyOn(sveltejsKitModule, 'error'); 9 | 10 | const mockNonExistingCodeSnippetId = 1; 11 | expect(() => 12 | throwCodeSnippetNotFoundError(mockNonExistingCodeSnippetId), 13 | ).toThrow(); 14 | expect(errorSpy).toHaveBeenCalledTimes(1); 15 | expect(errorSpy).toHaveBeenCalledWith( 16 | 404, 17 | 'Code snippet with ID 1 not found', 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export function throwCodeSnippetNotFoundError(codeSnippetId: number): never { 4 | throw error(404, `Code snippet with ID ${codeSnippetId} not found`); 5 | } 6 | 7 | export function throwCodeSnippetDeletionUnauthorizedError(): never { 8 | throw error(403, 'You are not authorized to delete this code snippet'); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/server/code-snippets/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | -------------------------------------------------------------------------------- /src/lib/server/core/hooks/check-mandatory-private-env-vars.handle.node-test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it, vi } from 'vitest'; 2 | 3 | import * as utilsModule from '../utils'; 4 | import { checkMandatoryPrivateEnvVarsHandle } from './check-mandatory-private-env-vars.handle'; 5 | 6 | describe(checkMandatoryPrivateEnvVarsHandle.name, () => { 7 | afterEach(() => { 8 | vi.restoreAllMocks(); 9 | }); 10 | 11 | it('should resolve', async () => { 12 | const spy = vi 13 | .spyOn(utilsModule, 'exitIfEnvVarsNotSet') 14 | .mockReturnValueOnce(); 15 | const resolve = vi.fn(); 16 | await checkMandatoryPrivateEnvVarsHandle({ 17 | event: {} as any, 18 | resolve, 19 | }); 20 | 21 | expect(spy).toHaveBeenCalledTimes(1); 22 | expect(resolve).toHaveBeenCalledTimes(1); 23 | }); 24 | 25 | it('should reject', async () => { 26 | const spy = vi 27 | .spyOn(utilsModule, 'exitIfEnvVarsNotSet') 28 | .mockImplementationOnce(() => { 29 | throw new Error(); 30 | }); 31 | const resolve = vi.fn(); 32 | 33 | await expect( 34 | checkMandatoryPrivateEnvVarsHandle({ 35 | event: {} as any, 36 | resolve, 37 | }), 38 | ).rejects.toThrow(new Error()); 39 | expect(spy).toHaveBeenCalledTimes(1); 40 | expect(resolve).toHaveBeenCalledTimes(0); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/lib/server/core/hooks/check-mandatory-private-env-vars.handle.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | 3 | import { exitIfEnvVarsNotSet } from '$lib/server/core/utils'; 4 | 5 | export const checkMandatoryPrivateEnvVarsHandle = (async ({ 6 | event, 7 | resolve, 8 | }) => { 9 | exitIfEnvVarsNotSet([ 10 | 'ORIGIN', 11 | 'DATABASE_URL', 12 | 'GOOGLE_OAUTH_APP_CLIENT_ID', 13 | 'GOOGLE_OAUTH_APP_CLIENT_SECRET', 14 | 'GOOGLE_OAUTH_APP_REDIRECT_URI', 15 | ]); 16 | 17 | return resolve(event); 18 | }) satisfies Handle; 19 | -------------------------------------------------------------------------------- /src/lib/server/core/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check-mandatory-private-env-vars.handle'; 2 | export * from './maintenance-mode.handle'; 3 | -------------------------------------------------------------------------------- /src/lib/server/core/hooks/maintenance-mode.handle.ts: -------------------------------------------------------------------------------- 1 | import { type Handle, redirect } from '@sveltejs/kit'; 2 | 3 | import { config } from '$lib/server/core/config'; 4 | import { 5 | encodeOriginalPath, 6 | ORIGINAL_PATH_URL_QUERY_PARAM_NAME, 7 | } from '$lib/shared/core/utils'; 8 | 9 | const NON_REDIRECTED_ROUTES = [ 10 | '/maintenance', 11 | // NOTE: Needed for SvelteKit page hydration, when requesting a route with 12 | // `+page.server.ts` file. 13 | // https://kit.svelte.dev/docs/glossary#hydration 14 | '/maintenance/__data.json', 15 | '/api/healthcheck', 16 | ]; 17 | 18 | export const maintenanceModeHandle = (async ({ event, resolve }) => { 19 | const originalUrl = new URL(event.request.url); 20 | if ( 21 | config.isMaintenanceMode && 22 | !NON_REDIRECTED_ROUTES.includes(originalUrl.pathname) 23 | ) { 24 | const encodedOriginalPath = encodeOriginalPath(originalUrl); 25 | throw redirect( 26 | 307, 27 | `/maintenance?${ORIGINAL_PATH_URL_QUERY_PARAM_NAME}=${encodedOriginalPath}`, 28 | ); 29 | } 30 | 31 | return resolve(event); 32 | }) satisfies Handle; 33 | -------------------------------------------------------------------------------- /src/lib/server/core/utils/env.utils.ts: -------------------------------------------------------------------------------- 1 | import { env as privateEnv } from '$env/dynamic/private'; 2 | import { env as publicEnv } from '$env/dynamic/public'; 3 | 4 | export function exitIfEnvVarsNotSet( 5 | envVars: string[], 6 | envType: 'private' | 'public' = 'private', 7 | ): void { 8 | const notSetEnvVars: string[] = []; 9 | envVars.forEach((envVar) => { 10 | if (!isEnvVarSet(envVar, envType)) { 11 | notSetEnvVars.push(envVar); 12 | } 13 | }); 14 | 15 | if (notSetEnvVars.length > 0) { 16 | const envTypeString = envType.charAt(0).toUpperCase() + envType.slice(1); 17 | console.error( 18 | `The following ${envTypeString} environment variables are not set: ${notSetEnvVars.join( 19 | ', ', 20 | )}`, 21 | ); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | export function throwIfEnvVarsNotSet( 27 | envVars: string[], 28 | envType: 'private' | 'public' = 'private', 29 | ): void { 30 | envVars.forEach((envVar) => { 31 | if (!isEnvVarSet(envVar, envType)) { 32 | const envTypeString = envType.charAt(0).toUpperCase() + envType.slice(1); 33 | throw new Error( 34 | `${envTypeString} environment variable ${envVar} must be set.`, 35 | ); 36 | } 37 | }); 38 | } 39 | 40 | function isEnvVarSet( 41 | envVar: string, 42 | envType: 'private' | 'public' = 'private', 43 | ): boolean { 44 | let env: any = privateEnv; 45 | if (envType === 'public') { 46 | env = publicEnv; 47 | } 48 | 49 | return ![undefined, ''].includes(env[envVar]); 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/server/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env.utils'; 2 | export * from './url.utils'; 3 | -------------------------------------------------------------------------------- /src/lib/server/core/utils/url.utils.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { getRefererHeaderUrl } from './url.utils'; 4 | 5 | describe(getRefererHeaderUrl.name, () => { 6 | it('should return null if request has no referer header', async () => { 7 | const request = { 8 | headers: new Headers(), 9 | } as Request; 10 | 11 | const refererUrl = getRefererHeaderUrl(request); 12 | 13 | expect(refererUrl).toEqual(null); 14 | }); 15 | 16 | it('should return URL from the referer header', async () => { 17 | const urlString = 'http://localhost:3000/path?param=value'; 18 | const request = { 19 | headers: new Headers({ 20 | referer: urlString, 21 | }), 22 | } as Request; 23 | 24 | const refererUrl = getRefererHeaderUrl(request); 25 | 26 | expect(refererUrl?.toString()).toEqual(urlString); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/server/core/utils/url.utils.ts: -------------------------------------------------------------------------------- 1 | export function getRefererHeaderUrl(request: Request): URL | null { 2 | const refererHeaderValue = request.headers.get('referer'); 3 | if (!refererHeaderValue) { 4 | return null; 5 | } 6 | const refererHeaderUrl = new URL(refererHeaderValue); 7 | 8 | return refererHeaderUrl; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/server/lucia/client.ts: -------------------------------------------------------------------------------- 1 | // Polyfill the Web Crypto API, required only for Node.js runtime <= version 18 2 | import 'lucia/polyfill/node'; 3 | 4 | import { prisma as prismaAdapter } from '@lucia-auth/adapter-prisma'; 5 | import { lucia } from 'lucia'; 6 | import { sveltekit as sveltekitMiddleware } from 'lucia/middleware'; 7 | 8 | import { dev } from '$app/environment'; 9 | import { prisma as prismaClient } from '$lib/server/prisma'; 10 | 11 | export const auth = lucia({ 12 | env: dev ? 'DEV' : 'PROD', 13 | middleware: sveltekitMiddleware(), 14 | adapter: prismaAdapter(prismaClient, { 15 | // Values are lowercase names of Prisma models 16 | user: 'user', 17 | session: 'session', 18 | key: 'key', 19 | }), 20 | // Properties of the database user included in the auth user as is 21 | getUserAttributes: (authUserSchema) => { 22 | return { 23 | email: authUserSchema.email, 24 | email_verified: authUserSchema.email_verified, 25 | created_at: authUserSchema.created_at!, 26 | }; 27 | }, 28 | // csrfProtection: false, 29 | }); 30 | -------------------------------------------------------------------------------- /src/lib/server/lucia/guards/auth-user.guard.node-test.ts: -------------------------------------------------------------------------------- 1 | import * as sveltejsKitModule from '@sveltejs/kit'; 2 | import { 3 | afterEach, 4 | beforeEach, 5 | describe, 6 | expect, 7 | it, 8 | type MockInstance, 9 | vi, 10 | } from 'vitest'; 11 | 12 | import { getMockAuthUser } from '$lib/shared/lucia/testing'; 13 | 14 | import { guardAuthUser } from './auth-user.guard'; 15 | 16 | describe(guardAuthUser.name, () => { 17 | let redirectSpy: MockInstance; 18 | 19 | beforeEach(() => { 20 | redirectSpy = vi.spyOn(sveltejsKitModule, 'redirect'); 21 | }); 22 | 23 | afterEach(async () => { 24 | vi.restoreAllMocks(); 25 | }); 26 | 27 | it('should return if user is authenticated', async () => { 28 | const locals = { 29 | authUser: getMockAuthUser(), 30 | } as App.Locals; 31 | const url = new URL('http://localhost'); 32 | 33 | const result = guardAuthUser(locals, url); 34 | 35 | expect(result).toEqual({ 36 | authUser: locals.authUser, 37 | }); 38 | expect(redirectSpy).toHaveBeenCalledTimes(0); 39 | }); 40 | 41 | it('should throw redirect if user is not authenticated', async () => { 42 | const locals = {} as App.Locals; 43 | const url = new URL('http://localhost/protected?param=value'); 44 | const redirectSpy = vi.spyOn(sveltejsKitModule, 'redirect'); 45 | 46 | expect(() => guardAuthUser(locals, url)).toThrow(); 47 | 48 | expect(redirectSpy).toHaveBeenCalledTimes(1); 49 | expect(redirectSpy).toHaveBeenCalledWith( 50 | 307, 51 | '/sign-in?originalPath=%2Fprotected%3Fparam%3Dvalue', 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/lib/server/lucia/guards/auth-user.guard.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | import { 4 | encodeOriginalPath, 5 | ORIGINAL_PATH_URL_QUERY_PARAM_NAME, 6 | } from '$lib/shared/core/utils'; 7 | 8 | export function guardAuthUser(locals: App.Locals, url: URL): App.PageData { 9 | if (locals.authUser) { 10 | return { 11 | authUser: locals.authUser, 12 | }; 13 | } 14 | 15 | const encodedOriginalPath = encodeOriginalPath(url); 16 | const redirectPath = `/sign-in?${ORIGINAL_PATH_URL_QUERY_PARAM_NAME}=${encodedOriginalPath}`; 17 | throw redirect(307, redirectPath); 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/server/lucia/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-user.guard'; 2 | -------------------------------------------------------------------------------- /src/lib/server/lucia/hooks/add-auth-data-to-local.handle.node-test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it, vi } from 'vitest'; 2 | 3 | import type * as clientModule from '../client'; 4 | import type * as utilsModule from '../utils'; 5 | import { addAuthDataToLocalHandle } from './add-auth-data-to-local.handle'; 6 | 7 | vi.mock('../client', async () => { 8 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 9 | const actual = (await vi.importActual('../client')) as typeof clientModule; 10 | return { 11 | ...actual, 12 | auth: { 13 | handleRequest: vi.fn().mockReturnValue('mock-auth-request'), 14 | }, 15 | }; 16 | }); 17 | 18 | vi.mock('../utils', async () => { 19 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 20 | const actual = (await vi.importActual('../utils')) as typeof utilsModule; 21 | return { 22 | ...actual, 23 | getCurrentAuthSession: vi.fn().mockReturnValue('mock-auth-session'), 24 | getCurrentAuthUserFromSession: vi.fn().mockReturnValue('mock-auth-user'), 25 | }; 26 | }); 27 | 28 | describe(addAuthDataToLocalHandle.name, () => { 29 | afterEach(async () => { 30 | vi.restoreAllMocks(); 31 | }); 32 | 33 | it('should set event.locals', async () => { 34 | const resolve = vi.fn(); 35 | const event = { 36 | locals: {}, 37 | }; 38 | await addAuthDataToLocalHandle({ 39 | event: event as any, 40 | resolve, 41 | }); 42 | 43 | expect(event.locals).toEqual({ 44 | authRequest: 'mock-auth-request', 45 | authSession: 'mock-auth-session', 46 | authUser: 'mock-auth-user', 47 | }); 48 | expect(resolve).toHaveBeenCalledTimes(1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/lib/server/lucia/hooks/add-auth-data-to-local.handle.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | 3 | import { auth } from '../client'; 4 | import { getCurrentAuthSession, getCurrentAuthUserFromSession } from '../utils'; 5 | 6 | export const addAuthDataToLocalHandle = (async ({ event, resolve }) => { 7 | // Initialize AuthRequest by performing CSRF check 8 | // and storing session ID extracted from session cookie. 9 | // We can pass `event` because we used the SvelteKit middleware. 10 | event.locals.authRequest = auth.handleRequest(event); 11 | event.locals.authSession = await getCurrentAuthSession( 12 | event.locals.authRequest, 13 | ); 14 | event.locals.authUser = getCurrentAuthUserFromSession( 15 | event.locals.authSession, 16 | ); 17 | 18 | return resolve(event); 19 | }) satisfies Handle; 20 | -------------------------------------------------------------------------------- /src/lib/server/lucia/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-auth-data-to-local.handle'; 2 | -------------------------------------------------------------------------------- /src/lib/server/lucia/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/lib/server/lucia/oauth/constants.ts: -------------------------------------------------------------------------------- 1 | export const OAUTH_TYPE_QUERY_PARAM_NAME = 'oauth-type'; 2 | export const OAUTH_STATE_COOKIE_MAX_AGE_IN_SECONDS = 60 * 60; // 1 hour 3 | -------------------------------------------------------------------------------- /src/lib/server/lucia/oauth/google/constants.ts: -------------------------------------------------------------------------------- 1 | // See `state` parameter: 2 | // https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#redirecting 3 | export const GOOGLE_OAUTH_STATE_COOKIE_NAME = 'google_oauth_state'; 4 | export const GOOGLE_OAUTH_STATE_SEPARATOR = '-----'; 5 | export const GOOGLE_OAUTH_STATE_QUERY_PARAM_NAME = 'state'; 6 | export const GOOGLE_OAUTH_CODE_QUERY_PARAM_NAME = 'code'; 7 | export const GOOGLE_OAUTH_TYPE_QUERY_PARAM_VALUE = 'google'; 8 | -------------------------------------------------------------------------------- /src/lib/server/lucia/oauth/google/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './provider'; 3 | -------------------------------------------------------------------------------- /src/lib/server/lucia/oauth/google/provider.ts: -------------------------------------------------------------------------------- 1 | import { google as googleProvider } from '@lucia-auth/oauth/providers'; 2 | 3 | import { config } from '$lib/server/core/config'; 4 | 5 | import { auth } from '../../client'; 6 | 7 | export const googleAuth = googleProvider(auth, { 8 | clientId: config.auth.google.clientId, 9 | clientSecret: config.auth.google.clientSecret, 10 | redirectUri: config.auth.google.redirectUri, 11 | scope: [ 12 | 'https://www.googleapis.com/auth/userinfo.email', 13 | 'https://www.googleapis.com/auth/userinfo.profile', 14 | 'openid', 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/server/lucia/oauth/google/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-in.utils'; 2 | -------------------------------------------------------------------------------- /src/lib/server/lucia/oauth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | -------------------------------------------------------------------------------- /src/lib/server/lucia/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { auth } from '../client'; 2 | 3 | export type Auth = typeof auth; 4 | -------------------------------------------------------------------------------- /src/lib/server/lucia/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { AuthRequest } from 'lucia'; 2 | 3 | import type { AuthSession, AuthUser } from '$lib/shared/lucia/types'; 4 | 5 | /** 6 | * NOTE: Results of Lucia's AuthRequest.validate() are cached, so almost always 7 | * returns a cached session validation result. 8 | */ 9 | export async function getCurrentAuthSession( 10 | authRequest: AuthRequest, 11 | ): Promise { 12 | // Validate and return session based on stored session ID. 13 | // Results are cached. 14 | return authRequest.validate(); 15 | } 16 | 17 | export async function getCurrentAuthUserFromRequest( 18 | authRequest: AuthRequest, 19 | ): Promise { 20 | const session = await getCurrentAuthSession(authRequest); 21 | return getCurrentAuthUserFromSession(session); 22 | } 23 | 24 | export function getCurrentAuthUserFromSession( 25 | session: AuthSession | null, 26 | ): AuthUser | null { 27 | return session?.user ?? null; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/server/pagination/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pagination-query'; 2 | -------------------------------------------------------------------------------- /src/lib/server/pagination/types/pagination-query.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationQuery { 2 | page?: number | undefined; 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/server/pagination/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { getUrlPathAndQueryParams } from '$lib/shared/core/utils'; 2 | 3 | import type { PaginationQuery } from '../types'; 4 | 5 | export function generatePreviousAndNextPageUrlPaths( 6 | currentUrl: URL, 7 | paginationQuery: PaginationQuery, 8 | totalPageCount: number, 9 | ): { 10 | previousPageUrlPath: string | undefined; 11 | nextPageUrlPath: string | undefined; 12 | } { 13 | let previousPageUrlPath: string | undefined = undefined; 14 | if (paginationQuery.page && paginationQuery.page > 1) { 15 | const previousPageSearchParams = new URLSearchParams( 16 | currentUrl.searchParams.toString(), 17 | ); 18 | if (paginationQuery.page > 2) { 19 | previousPageSearchParams.set('page', String(paginationQuery.page - 1)); 20 | } else { 21 | previousPageSearchParams.delete('page'); 22 | } 23 | 24 | const previousPageUrl = new URL(currentUrl.toString()); 25 | previousPageUrl.search = previousPageSearchParams.toString(); 26 | previousPageUrlPath = getUrlPathAndQueryParams(previousPageUrl); 27 | } 28 | 29 | let nextPageUrlPath: string | undefined = undefined; 30 | if ( 31 | totalPageCount > 1 && 32 | (paginationQuery.page == null || paginationQuery.page < totalPageCount) 33 | ) { 34 | const nextPageSearchParams = new URLSearchParams( 35 | currentUrl.searchParams.toString(), 36 | ); 37 | nextPageSearchParams.set('page', String((paginationQuery.page ?? 1) + 1)); 38 | 39 | const nextPageUrl = new URL(currentUrl.toString()); 40 | nextPageUrl.search = nextPageSearchParams.toString(); 41 | nextPageUrlPath = getUrlPathAndQueryParams(nextPageUrl); 42 | } 43 | 44 | return { 45 | previousPageUrlPath, 46 | nextPageUrlPath, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/server/posthog/client.ts: -------------------------------------------------------------------------------- 1 | import { PostHog } from 'posthog-node'; 2 | 3 | import { _setupPosthogClientBase } from '$lib/shared/posthog/utils'; 4 | 5 | export { PostHogSentryIntegration } from 'posthog-node'; 6 | 7 | export let posthog: PostHog | undefined; 8 | 9 | export function setupNodePosthogClient( 10 | projectApiKey: string | undefined, 11 | apiHost: string | undefined, 12 | ): PostHog | undefined { 13 | posthog = _setupPosthogClientBase( 14 | projectApiKey, 15 | apiHost, 16 | posthog, 17 | getNodePosthogClient, 18 | ); 19 | 20 | return posthog; 21 | } 22 | 23 | function getNodePosthogClient(projectApiKey: string, apiHost: string): PostHog { 24 | return new PostHog(projectApiKey, { 25 | host: apiHost, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/server/posthog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/lib/server/prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | export const prisma = new PrismaClient(); 4 | -------------------------------------------------------------------------------- /src/lib/server/prisma/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/lib/server/prisma/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '@prisma/client'; 2 | 3 | export type DatabaseUser = User; 4 | -------------------------------------------------------------------------------- /src/lib/server/roarr/hooks/http-log.handle.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | 3 | import { config } from '$lib/server/core/config'; 4 | import { roarr } from '$lib/server/roarr'; 5 | 6 | export const httpLogHandle = (async ({ event, resolve }) => { 7 | if (!config.roarr.isAccessLoggingEnabled) { 8 | return resolve(event); 9 | } 10 | 11 | const requestTimestamp = Date.now(); 12 | const response = await resolve(event); 13 | const responseTimeInMs = Date.now() - requestTimestamp; 14 | 15 | const { method, url, headers: requestHeaders } = event.request; 16 | const { status, headers: responseHeaders } = response; 17 | 18 | const contentLengthBytesString = responseHeaders.get('content-length'); 19 | const contentLengthInBytes: number | null = 20 | Number(contentLengthBytesString) || 0; 21 | 22 | roarr.info('Access log', { 23 | logType: 'http', 24 | request: { 25 | address: event.getClientAddress(), 26 | userId: event.locals.authUser?.userId ?? null, 27 | userAgent: requestHeaders.get('user-agent'), 28 | method, 29 | url, 30 | route: event.route.id, 31 | referrer: requestHeaders.get('referer') ?? requestHeaders.get('referrer'), 32 | }, 33 | response: { 34 | status: status, 35 | contentLengthInBytes, 36 | responseTimeInMs, 37 | }, 38 | }); 39 | 40 | return response; 41 | }) satisfies Handle; 42 | -------------------------------------------------------------------------------- /src/lib/server/roarr/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-log.handle'; 2 | -------------------------------------------------------------------------------- /src/lib/server/roarr/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/lib/server/roarr/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types partially copied from `node_modules/roarr/src/types.ts`. 3 | * Don't import from `node_modules/roarr/src/types.ts` directly to avoid type errors. 4 | */ 5 | 6 | import type { LogLevelName } from 'roarr'; 7 | 8 | import type { LoggerContext } from '$lib/shared/logging/types'; 9 | 10 | export type ServerLoggerLoggingMethodName = 11 | | ServerLoggerLoggingMethodNameNoOnce 12 | | ServerLoggerLoggingMethodNameOnce; 13 | 14 | export interface JsonObject { 15 | [k: string]: JsonValue; 16 | } 17 | export type JsonValue = 18 | | JsonObject 19 | | JsonValue[] 20 | | boolean 21 | | number 22 | | string 23 | | readonly JsonValue[] 24 | | null 25 | | undefined; 26 | 27 | export interface ServerLoggerContext extends JsonObject, LoggerContext { 28 | error?: JsonValue; 29 | sentryTraceId?: string; 30 | } 31 | 32 | export interface ServerLoggerContextWithError extends LoggerContext { 33 | error?: Error | JsonValue; 34 | sentryTraceId?: string; 35 | // WARN: Other properties should not have an `Error` type, but I don't know 36 | // how to enforce it in combination with the type of `error` property. 37 | [k: string]: Error | JsonValue; 38 | } 39 | 40 | type ServerLoggerLoggingMethodNameNoOnce = LogLevelName; 41 | type ServerLoggerLoggingMethodNameOnce = 42 | `${ServerLoggerLoggingMethodNameNoOnce}Once`; 43 | -------------------------------------------------------------------------------- /src/lib/server/roarr/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { SeverityLevel } from '@sentry/sveltekit'; 2 | 3 | import { sentry } from '$lib/shared/sentry'; 4 | 5 | import { roarr } from '../client'; 6 | 7 | export function logError(error: Error, level: SeverityLevel = 'error'): void { 8 | roarr.error(error.message, { error }, 4); 9 | sentry?.captureException(error, { level }); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/server/sentry/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './set-sentry-user-identity.handle'; 2 | -------------------------------------------------------------------------------- /src/lib/server/sentry/hooks/set-sentry-user-identity.handle.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | 3 | import { posthog, PostHogSentryIntegration } from '$lib/server/posthog'; 4 | import { sentry } from '$lib/shared/sentry'; 5 | 6 | export const setSentryUserIdentity = (async ({ event, resolve }) => { 7 | if (!sentry) { 8 | return resolve(event); 9 | } 10 | 11 | const authUser = event.locals.authUser; 12 | 13 | if (!authUser) { 14 | sentry.setUser(null); 15 | } else { 16 | sentry.setUser({ 17 | id: authUser.userId, 18 | email: authUser.email, 19 | }); 20 | 21 | if (posthog) { 22 | sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, authUser.userId); 23 | } 24 | } 25 | 26 | return resolve(event); 27 | }) satisfies Handle; 28 | -------------------------------------------------------------------------------- /src/lib/server/sentry/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/sveltekit'; 2 | import type { Integration } from '@sentry/types'; 3 | 4 | import { posthog, PostHogSentryIntegration } from '$lib/server/posthog'; 5 | import { prisma } from '$lib/server/prisma'; 6 | 7 | export function getServerSentryIntegrations( 8 | organization: string | undefined, 9 | ): Integration[] { 10 | const integrations: Integration[] = [ 11 | new Sentry.Integrations.Prisma({ client: prisma }), 12 | ]; 13 | 14 | if (posthog && organization) { 15 | integrations.unshift( 16 | new PostHogSentryIntegration(posthog, undefined, organization), 17 | ); 18 | } 19 | 20 | return integrations; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/server/superforms/testing/index.ts: -------------------------------------------------------------------------------- 1 | export function getMockFormData( 2 | data: Record = {}, 3 | ): () => Promise { 4 | const formData = new FormData(); 5 | formData.append('__superform_id', 'mock-superform-id'); 6 | Object.entries(data).forEach(([key, value]) => { 7 | formData.append(key, value); 8 | }); 9 | const formDataFn = () => Promise.resolve(formData); 10 | 11 | return formDataFn; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/server/sveltekit/testing/index.ts: -------------------------------------------------------------------------------- 1 | import type { Cookies, RequestEvent, ServerLoad } from '@sveltejs/kit'; 2 | 3 | export function getMockCookies(partial: Partial = {}): Cookies { 4 | return partial as Cookies; 5 | } 6 | 7 | export function getMockLocals(partial: Partial = {}): App.Locals { 8 | return partial as App.Locals; 9 | } 10 | 11 | export function getMockRequest(partial: Partial = {}): Request { 12 | return partial as Request; 13 | } 14 | 15 | /** 16 | * Use `PageServerLoadEvent` type from `./$types.ts` as `T` 17 | */ 18 | export function getMockPageServerLoadEvent[0]>( 19 | partial: Partial = {}, 20 | ): T { 21 | return partial as T; 22 | } 23 | 24 | /** 25 | * Use `RequestEvent` type from `./$types.ts` as `T` 26 | */ 27 | export function getMockRequestEvent( 28 | partial: Partial = {}, 29 | ): T { 30 | return partial as T; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/create-edit-code-snippet.dto.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { expectZodErrorMessages } from '$lib/shared/zod/testing'; 4 | 5 | import { createEditCodeSnippetFormSchema } from './create-edit-code-snippet.dto'; 6 | 7 | const formSchema = Object.keys({ 8 | createEditCodeSnippetFormSchema, 9 | })[0]!; 10 | 11 | describe(formSchema, () => { 12 | it('should pass valid object', async () => { 13 | expect(() => 14 | createEditCodeSnippetFormSchema.parse({ 15 | name: 'mock-name', 16 | code: 'mock-code', 17 | }), 18 | ).not.toThrow(); 19 | }); 20 | 21 | it('should fail object with empty name and code', async () => { 22 | expectZodErrorMessages(() => { 23 | createEditCodeSnippetFormSchema.parse({ 24 | name: '', 25 | code: '', 26 | }); 27 | }, ['Code is required', 'Name is required']); 28 | }); 29 | 30 | it('should fail object with missing name and code', async () => { 31 | expectZodErrorMessages(() => { 32 | createEditCodeSnippetFormSchema.parse({}); 33 | }, ['Code is required', 'Name is required']); 34 | }); 35 | 36 | it('should fail object with an unknown property', async () => { 37 | expectZodErrorMessages(() => { 38 | createEditCodeSnippetFormSchema.parse({ 39 | name: 'mock-name', 40 | code: 'mock-code', 41 | unknownProperty: 'unknown-property', 42 | }); 43 | }, ["Unrecognized key(s) in object: 'unknownProperty'"]); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/create-edit-code-snippet.dto.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { getRequiredErrorMessage } from '$lib/shared/zod/utils'; 4 | 5 | const nameErrorMessage = getRequiredErrorMessage('Name'); 6 | const codeErrorMessage = getRequiredErrorMessage('Code'); 7 | export const createEditCodeSnippetFormSchema = z 8 | .object({ 9 | name: z 10 | .string({ required_error: nameErrorMessage }) 11 | .min(1, nameErrorMessage), 12 | code: z 13 | .string({ required_error: codeErrorMessage }) 14 | .min(1, codeErrorMessage), 15 | }) 16 | .strict(); 17 | export type CreateEditCodeSnippetFormSchema = 18 | typeof createEditCodeSnippetFormSchema; 19 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/delete-code-snippet.dto.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { expectZodErrorMessages } from '$lib/shared/zod/testing'; 4 | 5 | import { deleteCodeSnippetFormSchema } from './delete-code-snippet.dto'; 6 | 7 | const formSchema = Object.keys({ 8 | deleteCodeSnippetFormSchema, 9 | })[0]!; 10 | 11 | describe(formSchema, () => { 12 | it('should pass valid object', async () => { 13 | expect(() => 14 | deleteCodeSnippetFormSchema.parse({ 15 | id: 1, 16 | }), 17 | ).not.toThrow(); 18 | }); 19 | 20 | it('should fail object with missing name and code', async () => { 21 | expectZodErrorMessages(() => { 22 | deleteCodeSnippetFormSchema.parse({}); 23 | }, ['ID is required']); 24 | }); 25 | 26 | it('should fail object with an unknown property', async () => { 27 | expectZodErrorMessages(() => { 28 | deleteCodeSnippetFormSchema.parse({ 29 | id: 1, 30 | unknownProperty: 'unknown-property', 31 | }); 32 | }, ["Unrecognized key(s) in object: 'unknownProperty'"]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/delete-code-snippet.dto.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { getRequiredErrorMessage } from '$lib/shared/zod/utils'; 4 | 5 | export const deleteCodeSnippetFormSchema = z 6 | .object({ 7 | id: z.number({ required_error: getRequiredErrorMessage('ID') }), 8 | }) 9 | .strict(); 10 | export type DeleteCodeSnippetFormSchema = typeof deleteCodeSnippetFormSchema; 11 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/find-code-snippets.dto.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { expectZodErrorMessages } from '$lib/shared/zod/testing'; 4 | 5 | import { 6 | findCodeSnippetsDto, 7 | type FindCodeSnippetsQuery, 8 | } from './find-code-snippets.dto'; 9 | 10 | const formSchema = Object.keys({ 11 | findCodeSnippetsDto, 12 | })[0]!; 13 | 14 | describe(formSchema, () => { 15 | it('should pass valid object with minimum required properties', async () => { 16 | expect(() => 17 | findCodeSnippetsDto.parse({ count: 10 } as FindCodeSnippetsQuery), 18 | ).not.toThrow(); 19 | }); 20 | 21 | it('should pass valid object with all properties', async () => { 22 | expect(() => 23 | findCodeSnippetsDto.parse({ 24 | page: 1, 25 | count: 10, 26 | } as FindCodeSnippetsQuery), 27 | ).not.toThrow(); 28 | }); 29 | 30 | it('should fail object with wrong property values', async () => { 31 | expectZodErrorMessages(() => { 32 | findCodeSnippetsDto.parse({ 33 | page: 0, 34 | count: -1, 35 | }); 36 | }, ['Count must be >= 0', 'Page number must be > 0']); 37 | }); 38 | 39 | it('should fail object if count is 0 and page is greater than 1', async () => { 40 | expectZodErrorMessages(() => { 41 | findCodeSnippetsDto.parse({ 42 | page: 2, 43 | count: 0, 44 | } as FindCodeSnippetsQuery); 45 | }, ['Count cannot be 0 if page is greater than 1']); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/find-code-snippets.dto.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const findCodeSnippetsDto = z 4 | .object({ 5 | page: z.number().positive('Page number must be > 0').optional(), 6 | count: z.number().nonnegative('Count must be >= 0').optional(), 7 | filterBy: z.enum(['author']).optional(), 8 | filterValue: z.any().optional(), 9 | sortBy: z.enum(['created_at']).optional(), 10 | sortOrder: z.enum(['asc', 'desc']).optional(), 11 | }) 12 | .refine( 13 | (data) => { 14 | if (data.page && data.page > 1) { 15 | return data.count == null || data.count > 0; 16 | } 17 | 18 | return true; 19 | }, 20 | { 21 | message: 'Count cannot be 0 if page is greater than 1', 22 | path: ['count'], 23 | }, 24 | ); 25 | export type FindCodeSnippetsDto = typeof findCodeSnippetsDto; 26 | export type FindCodeSnippetsQuery = z.infer; 27 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-edit-code-snippet.dto'; 2 | export * from './delete-code-snippet.dto'; 3 | export * from './find-code-snippets.dto'; 4 | -------------------------------------------------------------------------------- /src/lib/shared/code-snippets/testing/index.ts: -------------------------------------------------------------------------------- 1 | import type { CodeSnippet } from '@prisma/client'; 2 | import type { SuperValidated } from 'sveltekit-superforms'; 3 | 4 | import type { CreateEditCodeSnippetFormSchema } from '../dtos'; 5 | 6 | export function getMockCodeSnippet( 7 | overrides?: Partial, 8 | ): CodeSnippet { 9 | return { 10 | id: 1, 11 | user_id: 'mock-user-id', 12 | name: 'mock-name', 13 | code: 'mock-code', 14 | is_deleted: false, 15 | created_at: new Date(), 16 | updated_at: new Date(), 17 | deleted_at: null, 18 | ...overrides, 19 | }; 20 | } 21 | 22 | export function getMockCreateCodeSnippetFormConstraints(): Partial< 23 | SuperValidated 24 | > { 25 | return { 26 | constraints: { 27 | code: { 28 | minlength: 1, 29 | required: true, 30 | }, 31 | name: { 32 | minlength: 1, 33 | required: true, 34 | }, 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/shared/core/testing/index.ts: -------------------------------------------------------------------------------- 1 | export function getMockWithType(event: Partial): T { 2 | return event as T; 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/datetime.utils.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { formatUtcDateTime } from './datetime.utils'; 4 | 5 | describe(formatUtcDateTime.name, () => { 6 | it('should format UTC date time', async () => { 7 | const date = new Date('2021-01-01T01:00:00+01:00'); 8 | 9 | expect(formatUtcDateTime(date)).toEqual('2021-01-01T00:00Z'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/datetime.utils.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import utc from 'dayjs/plugin/utc'; 3 | 4 | dayjs.extend(utc); 5 | 6 | export { dayjs }; 7 | 8 | export function formatUtcDateTime(date: Date): string { 9 | return dayjs(date).utc().format('YYYY-MM-DDTHH:mm[Z]'); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './datetime.utils'; 2 | export * from './parsing.utils'; 3 | export * from './url.utils'; 4 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/parsing.utils.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | attemptToParseAsNumber, 5 | isStringParsableAsNumber, 6 | } from './parsing.utils'; 7 | 8 | describe(isStringParsableAsNumber.name, () => { 9 | it('should return true', async () => { 10 | expect(isStringParsableAsNumber('42')).toEqual(true); 11 | }); 12 | 13 | it('should return false', async () => { 14 | expect(isStringParsableAsNumber('42a')).toEqual(false); 15 | }); 16 | }); 17 | 18 | describe(attemptToParseAsNumber.name, () => { 19 | it('should return a number', async () => { 20 | expect(attemptToParseAsNumber('42')).toEqual(42); 21 | }); 22 | 23 | it('should return undefined for invalid string', async () => { 24 | expect(attemptToParseAsNumber('42a')).toEqual(undefined); 25 | }); 26 | 27 | it('should return undefined for null', async () => { 28 | expect(attemptToParseAsNumber(null)).toEqual(undefined); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/parsing.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Useful for parsing return value of `URLSearchParams.get()` 3 | */ 4 | export function attemptToParseAsNumber( 5 | value: string | null, 6 | ): number | undefined { 7 | if (value && isStringParsableAsNumber(value)) { 8 | return Number(value); 9 | } 10 | 11 | return undefined; 12 | } 13 | 14 | export function isStringParsableAsNumber(value: string): boolean { 15 | return !isNaN(Number(value)); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/url.utils.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | decodeOriginalPath, 5 | encodeOriginalPath, 6 | getUrlPathAndQueryParams, 7 | ORIGINAL_PATH_URL_QUERY_PARAM_NAME, 8 | } from './url.utils'; 9 | 10 | describe(getUrlPathAndQueryParams.name, () => { 11 | it('should return path and query params', async () => { 12 | const url = new URL('http://localhost:3000/some/path?param=value'); 13 | 14 | const urlPathAndQueryParams = getUrlPathAndQueryParams(url); 15 | 16 | expect(urlPathAndQueryParams).toEqual('/some/path?param=value'); 17 | }); 18 | }); 19 | 20 | describe(encodeOriginalPath.name, () => { 21 | it('should encode path and query params', async () => { 22 | const url = new URL('http://localhost:3000/protected?param=value'); 23 | 24 | const encodedOriginalPath = encodeOriginalPath(url); 25 | 26 | expect(encodedOriginalPath).toEqual('%2Fprotected%3Fparam%3Dvalue'); 27 | }); 28 | 29 | it('should encode root path', async () => { 30 | const url = new URL('http://localhost:3000'); 31 | 32 | const encodedOriginalPath = encodeOriginalPath(url); 33 | 34 | expect(encodedOriginalPath).toEqual('%2F'); 35 | }); 36 | }); 37 | 38 | describe(decodeOriginalPath.name, () => { 39 | it('should decode path and query params', async () => { 40 | const url = new URL( 41 | `http://localhost:3000/sign-in?${ORIGINAL_PATH_URL_QUERY_PARAM_NAME}=%2Fprotected%3Fparam%3Dvalue`, 42 | ); 43 | 44 | const originalPath = decodeOriginalPath(url); 45 | 46 | expect(originalPath).toEqual('/protected?param=value'); 47 | }); 48 | 49 | it('should return null', async () => { 50 | const url = new URL('http://localhost:3000/sign-in?param=value'); 51 | 52 | const originalPath = decodeOriginalPath(url); 53 | 54 | expect(originalPath).toEqual(null); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/lib/shared/core/utils/url.utils.ts: -------------------------------------------------------------------------------- 1 | export const ORIGINAL_PATH_URL_QUERY_PARAM_NAME = 'originalPath'; 2 | 3 | export function getUrlPathAndQueryParams(url: URL): string { 4 | return `${url.pathname}${url.search}`; 5 | } 6 | 7 | export function encodeOriginalPath(url: URL): string { 8 | return encodeURIComponent(getUrlPathAndQueryParams(url)); 9 | } 10 | 11 | export function decodeOriginalPath(url: URL): string | null { 12 | const encodedOriginalPath = url.searchParams.get( 13 | ORIGINAL_PATH_URL_QUERY_PARAM_NAME, 14 | ); 15 | const originalPath = 16 | encodedOriginalPath && decodeURIComponent(encodedOriginalPath); 17 | 18 | return originalPath; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/shared/logging/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface LoggerContext { 2 | sentryTraceId?: string; 3 | [k: string]: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/shared/logging/utils/index.ts: -------------------------------------------------------------------------------- 1 | import callsites from 'callsites'; 2 | 3 | import { sentry } from '$lib/shared/sentry'; 4 | import { getTraceId } from '$lib/shared/sentry/utils'; 5 | 6 | import type { LoggerContext } from '../types'; 7 | 8 | export function enrichLoggerContextWithSentryTraceId( 9 | context: T, 10 | ): T { 11 | if (!sentry) { 12 | return { ...context }; 13 | } 14 | 15 | const traceId = getTraceId(); 16 | if (!traceId) { 17 | return { ...context }; 18 | } 19 | 20 | return { 21 | ...context, 22 | sentryTraceId: traceId, 23 | }; 24 | } 25 | 26 | export function enrichContextWithDebugInfo( 27 | context: LoggerContext = {}, 28 | rootFolder = '', 29 | stackLevel = 3, 30 | ): LoggerContext { 31 | return { 32 | ...context, 33 | callName: getCallName(stackLevel), 34 | fileName: getFileName(rootFolder, stackLevel), 35 | }; 36 | } 37 | 38 | function getCallName(stackLevel = 3): string { 39 | const typeName = callsites()[stackLevel]?.getTypeName() ?? ''; 40 | const functionName = 41 | callsites()[3]?.getFunctionName() ?? 42 | callsites()[stackLevel]?.getMethodName() ?? 43 | ''; 44 | 45 | if (typeName) { 46 | return `${typeName}.${functionName}`; 47 | } 48 | 49 | return functionName; 50 | } 51 | 52 | function getFileName(rootFolder = '', stackLevel = 3): string { 53 | const fileName = 54 | callsites()[stackLevel]?.getFileName() ?? 55 | callsites()[stackLevel]?.getEvalOrigin() ?? 56 | ''; 57 | 58 | return fileName.replace(rootFolder, ''); 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/shared/lucia/testing/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import type { AuthRequest } from 'lucia'; 3 | import { vi } from 'vitest'; 4 | 5 | import type { AuthSession, AuthUser } from '$lib/shared/lucia/types'; 6 | 7 | export function getMockAuthRequest( 8 | authSession: AuthSession | null = null, 9 | ): AuthRequest { 10 | return { 11 | validate: vi.fn().mockResolvedValue(authSession), 12 | setSession: vi.fn(), 13 | } as Partial as AuthRequest; 14 | } 15 | 16 | export function getMockAuthSession( 17 | overrides?: Partial | undefined, 18 | ): AuthSession { 19 | const currentTimestamp = dayjs(); 20 | const tommorowTimestamp = currentTimestamp.add(1, 'day'); 21 | const timestampIn2Weeks = currentTimestamp.add(2, 'week'); 22 | return { 23 | sessionId: 'mock-session-id', 24 | activePeriodExpiresAt: tommorowTimestamp.toDate(), 25 | idlePeriodExpiresAt: timestampIn2Weeks.toDate(), 26 | state: 'active', 27 | fresh: false, 28 | ...overrides, 29 | user: { 30 | ...getMockAuthUser(overrides?.user), 31 | }, 32 | }; 33 | } 34 | 35 | export function getMockAuthUser( 36 | overrides?: Partial | undefined, 37 | ): AuthUser { 38 | return { 39 | email: 'mock-email', 40 | email_verified: true, 41 | userId: 'mock-user-id', 42 | created_at: new Date(), 43 | ...overrides, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/shared/lucia/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Session, SessionSchema, User, UserSchema } from 'lucia'; 2 | 3 | export type AuthSession = Session; 4 | export type AuthSessionSchema = SessionSchema; 5 | 6 | export type AuthUser = User; 7 | export type AuthUserSchema = UserSchema; 8 | -------------------------------------------------------------------------------- /src/lib/shared/posthog/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const POSTHOG_PAGE_VIEW_EVENT_NAME = '$pageview'; 2 | export const POSTHOG_PAGE_LEAVE_EVENT_NAME = '$pageleave'; 3 | 4 | export const POSTHOG_USER_SIGN_UP_EVENT_NAME = 'user--sign-up'; 5 | export const POSTHOG_USER_SIGN_IN_EVENT_NAME = 'user--sign-in'; 6 | export const POSTHOG_USER_SIGN_OUT_EVENT_NAME = 'user--sign-out'; 7 | export const POSTHOG_CODE_SNIPPET_CREATED_EVENT_NAME = 'code-snippet--created'; 8 | export const POSTHOG_CODE_SNIPPET_DELETED_EVENT_NAME = 'code-snippet--deleted'; 9 | export const POSTHOG_CODE_SNIPPET_UPDATED_EVENT_NAME = 'code-snippet--updated'; 10 | -------------------------------------------------------------------------------- /src/lib/shared/posthog/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | export function checkIfPosthogClientConfigured( 4 | posthog: T | undefined, 5 | ): void { 6 | if (!posthog) { 7 | throw new Error('Posthog client is not configured'); 8 | } 9 | } 10 | 11 | export function _setupPosthogClientBase( 12 | projectApiKey: string | undefined, 13 | apiHost: string | undefined, 14 | posthog: T | undefined, 15 | getClient: (projectApiKey: string, apiHost: string) => T, 16 | ): T | undefined { 17 | if (!posthog) { 18 | if (!_arePosthogClientConfigurationInputsValid(projectApiKey, apiHost)) { 19 | displayPosthogSetupWarning(); 20 | 21 | return undefined; 22 | } 23 | 24 | return getClient(projectApiKey!, apiHost!); 25 | } 26 | 27 | return posthog; 28 | } 29 | 30 | export function _arePosthogClientConfigurationInputsValid( 31 | projectApiKey: string | undefined, 32 | apiHost: string | undefined, 33 | ): boolean { 34 | const projectApiKeyIsValid = !!projectApiKey; 35 | const apiHostIsValid = 36 | !!apiHost && 37 | ['https://app.posthog.com', 'https://eu.posthog.com'].includes(apiHost); 38 | 39 | return projectApiKeyIsValid && apiHostIsValid; 40 | } 41 | 42 | function displayPosthogSetupWarning(): void { 43 | if (dev) { 44 | console.warn( 45 | 'Posthog project API key and/or API host are invalid or not set. Posthog will not be configured.', 46 | ); 47 | return; 48 | } 49 | 50 | console.warn('Analytics not configured.'); 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/shared/sentry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/lib/shared/sentry/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { SeverityLevel } from '@sentry/sveltekit'; 2 | -------------------------------------------------------------------------------- /src/lib/shared/sentry/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { SpanContextData } from '@sentry/types'; 2 | 3 | import { sentry } from '../client'; 4 | 5 | export function getTraceSpanId(): string | undefined { 6 | const { traceId, spanId } = getSpanContextData() || {}; 7 | if (!traceId || !spanId) { 8 | return; 9 | } 10 | 11 | return `${traceId}-${spanId}`; 12 | } 13 | 14 | export function getTraceId(): string | undefined { 15 | const { traceId } = getSpanContextData() || {}; 16 | return traceId; 17 | } 18 | 19 | export function getSpanId(): string | undefined { 20 | const { spanId } = getSpanContextData() || {}; 21 | return spanId; 22 | } 23 | 24 | export function getSpanContextData(): SpanContextData | undefined { 25 | return sentry?.getActiveSpan()?.spanContext(); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/shared/superforms/testing/index.ts: -------------------------------------------------------------------------------- 1 | import type { SuperValidated, ZodValidation } from 'sveltekit-superforms'; 2 | import type { AnyZodObject } from 'zod'; 3 | 4 | export function getMockFormValue< 5 | T extends ZodValidation, 6 | M = App.Superforms.Message, 7 | >(overrides?: Partial>): SuperValidated { 8 | const defaultValue = { 9 | id: 'mock-superform-id', 10 | constraints: {}, 11 | data: {}, 12 | errors: {}, 13 | posted: true, 14 | valid: false, 15 | } as SuperValidated; 16 | 17 | let value = { ...defaultValue }; 18 | if (overrides) { 19 | value = Object.assign(value, overrides); 20 | } 21 | 22 | return value; 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/shared/sveltekit/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test-helpers'; 2 | -------------------------------------------------------------------------------- /src/lib/shared/zod/testing/index.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | import { ZodError } from 'zod'; 3 | 4 | export function expectZodErrorMessages( 5 | callback: () => any, 6 | messages: string[], 7 | ) { 8 | try { 9 | callback(); 10 | } catch (e) { 11 | const error = e as ZodError; 12 | if (!(error instanceof ZodError)) { 13 | throw e; 14 | } 15 | 16 | const errorMessages = error.issues.map((issue) => issue.message).sort(); 17 | expect(errorMessages).toEqual(messages); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/shared/zod/utils/errors.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { getRequiredErrorMessage } from './errors'; 4 | 5 | describe(getRequiredErrorMessage.name, () => { 6 | it('should return an error message', async () => { 7 | expect(getRequiredErrorMessage('mock-field-name')).toEqual( 8 | 'mock-field-name is required', 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/lib/shared/zod/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export function getRequiredErrorMessage(fieldName: string) { 2 | return `${fieldName} is required`; 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/shared/zod/utils/extract.node-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { z, ZodError } from 'zod'; 3 | 4 | import { extractZodErrorPaths } from './extract'; 5 | 6 | describe(extractZodErrorPaths.name, () => { 7 | it('should return error path if error is thrown', () => { 8 | const mockError = new ZodError([ 9 | { 10 | code: 'too_small', 11 | minimum: 0, 12 | type: 'number', 13 | inclusive: false, 14 | exact: false, 15 | message: 'Page number must be > 0', 16 | path: ['page'], 17 | }, 18 | ]); 19 | 20 | const errorPaths = extractZodErrorPaths(mockError); 21 | 22 | expect(errorPaths).toEqual([['page']]); 23 | }); 24 | 25 | it('should return error path if error is thrown', () => { 26 | const schema = z.object({ 27 | name: z.string().min(1), 28 | age: z.number().min(18), 29 | }); 30 | const result = schema.safeParse({ 31 | name: '', 32 | age: 17, 33 | }); 34 | if (result.success) { 35 | throw new Error('Expected error to be thrown'); 36 | } 37 | 38 | const errorPaths = extractZodErrorPaths(result.error); 39 | 40 | expect(errorPaths).toEqual([['name'], ['age']]); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/lib/shared/zod/utils/extract.ts: -------------------------------------------------------------------------------- 1 | import type { ZodError } from 'zod'; 2 | 3 | export function extractZodErrorPaths(error: ZodError): (string | number)[][] { 4 | const errorPaths = error.issues.map((issue) => issue.path); 5 | 6 | return errorPaths; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/shared/zod/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | export * from './extract'; 3 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/code-snippets/[id]/edit/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | Edit - {data.form.data.name}{config.pageTitleSuffix} 24 | 28 | 29 | 30 | 35 | 40 | 41 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/code-snippets/[id]/edit/page.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import type { ToastStore } from '@skeletonlabs/skeleton'; 2 | import * as skeletonlabsSkeletonModule from '@skeletonlabs/skeleton'; 3 | import { cleanup, render } from '@testing-library/svelte'; 4 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | import type { CreateEditCodeSnippetFormSchema } from '$lib/shared/code-snippets/dtos'; 7 | import { getMockCreateCodeSnippetFormConstraints } from '$lib/shared/code-snippets/testing'; 8 | import { getMockFormValue } from '$lib/shared/superforms/testing'; 9 | 10 | import Component from './+page.svelte'; 11 | 12 | describe(Component.name, () => { 13 | beforeEach(() => { 14 | vi.spyOn(skeletonlabsSkeletonModule, 'getToastStore').mockReturnValue({ 15 | trigger: vi.fn(), 16 | } as Partial as ToastStore); 17 | }); 18 | 19 | afterEach(() => { 20 | cleanup(); 21 | vi.restoreAllMocks(); 22 | }); 23 | 24 | it('should render component', () => { 25 | const renderResult = render(Component, { 26 | props: { 27 | data: { 28 | authUser: null, 29 | form: getMockFormValue({ 30 | ...getMockCreateCodeSnippetFormConstraints(), 31 | data: { 32 | name: '', 33 | code: '', 34 | }, 35 | }), 36 | }, 37 | }, 38 | }); 39 | 40 | expect(renderResult.component).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/code-snippets/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | Create code snippet{config.pageTitleSuffix} 11 | 12 | 13 | 14 | 19 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/code-snippets/create/page.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import type { ToastStore } from '@skeletonlabs/skeleton'; 2 | import * as skeletonlabsSkeletonModule from '@skeletonlabs/skeleton'; 3 | import { cleanup, render } from '@testing-library/svelte'; 4 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | import type { CreateEditCodeSnippetFormSchema } from '$lib/shared/code-snippets/dtos'; 7 | import { getMockCreateCodeSnippetFormConstraints } from '$lib/shared/code-snippets/testing'; 8 | import { getMockFormValue } from '$lib/shared/superforms/testing'; 9 | 10 | import Component from './+page.svelte'; 11 | 12 | describe(Component.name, () => { 13 | beforeEach(() => { 14 | vi.spyOn(skeletonlabsSkeletonModule, 'getToastStore').mockReturnValue({ 15 | trigger: vi.fn(), 16 | } as Partial as ToastStore); 17 | }); 18 | 19 | afterEach(() => { 20 | cleanup(); 21 | vi.restoreAllMocks(); 22 | }); 23 | 24 | it('should render component', () => { 25 | const renderResult = render(Component, { 26 | props: { 27 | data: { 28 | authUser: null, 29 | form: getMockFormValue({ 30 | ...getMockCreateCodeSnippetFormConstraints(), 31 | data: { 32 | name: '', 33 | code: '', 34 | }, 35 | }), 36 | }, 37 | }, 38 | }); 39 | 40 | expect(renderResult.component).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/layout.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import { SlotTest } from '$lib/client/components/testing'; 5 | 6 | import Component from './+layout.svelte'; 7 | 8 | describe(Component.name, () => { 9 | afterEach(() => { 10 | cleanup(); 11 | }); 12 | 13 | it('should render the component', () => { 14 | const renderResult = render(Component); 15 | 16 | expect(renderResult.component).toBeTruthy(); 17 | }); 18 | 19 | it('should render slot content', () => { 20 | const renderResult = render(SlotTest, { props: { component: Component } }); 21 | 22 | expect(renderResult.getByTestId('slot-content')).toHaveTextContent( 23 | 'mock-slot-text', 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/profile/+page.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | Profile{config.pageTitleSuffix} 36 | 37 | 38 | 39 | 40 | 41 |

Profile

42 |
43 | 44 |
45 |

{data.authUser?.email}

46 |
47 | 54 |
55 | 56 | {#if $message} 57 | 60 | {/if} 61 |
62 |
63 | -------------------------------------------------------------------------------- /src/routes/(app)/(card-layout)/profile/page.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import type { ToastStore } from '@skeletonlabs/skeleton'; 2 | import * as skeletonlabsSkeletonModule from '@skeletonlabs/skeleton'; 3 | import { 4 | cleanup, 5 | queries, 6 | render, 7 | type RenderResult, 8 | } from '@testing-library/svelte'; 9 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 10 | 11 | import type { AuthUser } from '$lib/shared/lucia/types'; 12 | import { getMockFormValue } from '$lib/shared/superforms/testing'; 13 | 14 | import type { FormSchema } from './+page.server'; 15 | import Component from './+page.svelte'; 16 | 17 | describe(Component.name, () => { 18 | let renderResult: RenderResult; 19 | 20 | beforeEach(() => { 21 | vi.spyOn(skeletonlabsSkeletonModule, 'getToastStore').mockReturnValue({ 22 | trigger: vi.fn(), 23 | } as Partial as ToastStore); 24 | renderResult = render(Component, { 25 | props: { 26 | data: { 27 | authUser: { email: 'user@example.com' } as AuthUser, 28 | form: getMockFormValue(), 29 | }, 30 | }, 31 | }); 32 | }); 33 | 34 | afterEach(() => { 35 | cleanup(); 36 | vi.restoreAllMocks(); 37 | }); 38 | 39 | it('should display users email', () => { 40 | expect(renderResult.getByText('user@example.com')).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/routes/(app)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/(app)/layout.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import { SlotTest } from '$lib/client/components/testing'; 5 | 6 | import Component from './+layout.svelte'; 7 | 8 | describe(Component.name, () => { 9 | afterEach(() => { 10 | cleanup(); 11 | }); 12 | 13 | it('should render the component', () => { 14 | const renderResult = render(Component); 15 | 16 | expect(renderResult.component).toBeTruthy(); 17 | }); 18 | 19 | it('should render slot content', () => { 20 | const renderResult = render(SlotTest, { props: { component: Component } }); 21 | 22 | expect(renderResult.getByTestId('slot-content')).toHaveTextContent( 23 | 'mock-slot-text', 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/routes/(app)/page.CodeSnippetCard.mock.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
mock-code-snippet-card
25 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | {title}{config.pageTitleSuffix} 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { loadFlash } from 'sveltekit-flash-message/server'; 2 | 3 | export const load = loadFlash(({ locals }) => { 4 | // NOTE: Used only for the sibling `+error.svelte` that calls 5 | // `invalidateAll`. ALWAYS add this auth block to each nested page that 6 | // is accessible to unauthenticated users, but may conditionally display 7 | // sensitive data to authenticated users. 8 | const authUser = locals.authUser; 9 | 10 | return { 11 | authUser, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /src/routes/api/healthcheck/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | 3 | import { config } from '$lib/server/core/config'; 4 | 5 | import type { RequestHandler } from './$types'; 6 | 7 | export const GET = (() => { 8 | let status = 'OK'; 9 | if (config.isMaintenanceMode) { 10 | status = 'maintenance'; 11 | } 12 | 13 | return json({ status }); 14 | }) satisfies RequestHandler; 15 | -------------------------------------------------------------------------------- /src/routes/api/healthcheck/server.node-test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it, vi } from 'vitest'; 2 | 3 | import * as libServerCoreConfigModule from '$lib/server/core/config'; 4 | 5 | import { GET } from './+server'; 6 | 7 | describe(GET.name, () => { 8 | afterEach(async () => { 9 | vi.restoreAllMocks(); 10 | }); 11 | 12 | it('should return OK status', async () => { 13 | vi.spyOn(libServerCoreConfigModule, 'config', 'get').mockReturnValue({ 14 | isMaintenanceMode: false, 15 | } as Partial< 16 | typeof libServerCoreConfigModule.config 17 | > as typeof libServerCoreConfigModule.config); 18 | 19 | const response = GET(); 20 | 21 | expect(await response.json()).toEqual({ 22 | status: 'OK', 23 | }); 24 | }); 25 | 26 | it('should return maintenance status', async () => { 27 | vi.spyOn(libServerCoreConfigModule, 'config', 'get').mockReturnValue({ 28 | isMaintenanceMode: true, 29 | } as Partial< 30 | typeof libServerCoreConfigModule.config 31 | > as typeof libServerCoreConfigModule.config); 32 | 33 | const response = GET(); 34 | 35 | expect(await response.json()).toEqual({ 36 | status: 'maintenance', 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/routes/layout.server.node-test.ts: -------------------------------------------------------------------------------- 1 | import type { Cookies } from '@sveltejs/kit'; 2 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 3 | 4 | import { auth } from '$lib/server/lucia'; 5 | import { getMockAuthSession } from '$lib/shared/lucia/testing'; 6 | 7 | import { load } from './+layout.server'; 8 | import type { LayoutServerLoadEvent } from './$types'; 9 | 10 | describe(load.name, () => { 11 | beforeEach(async () => { 12 | vi.spyOn(auth, 'invalidateSession').mockResolvedValue(undefined); 13 | }); 14 | 15 | afterEach(async () => { 16 | vi.restoreAllMocks(); 17 | }); 18 | 19 | it('should return authUser when user is authenticated', async () => { 20 | const mockAuthSession = getMockAuthSession(); 21 | const mockLocals = { 22 | authUser: mockAuthSession.user, 23 | } as App.Locals; 24 | const mockCookies = { 25 | get: vi.fn(), 26 | } as Partial; 27 | const mockServerLoadEvent = { 28 | locals: mockLocals, 29 | cookies: mockCookies, 30 | } as LayoutServerLoadEvent; 31 | 32 | const result = await load(mockServerLoadEvent); 33 | 34 | expect(result).toEqual({ 35 | authUser: mockAuthSession.user, 36 | }); 37 | expect(mockCookies.get).toHaveBeenCalledTimes(1); 38 | expect(mockCookies.get).toHaveBeenCalledWith('flash'); 39 | }); 40 | 41 | it('should return null when user is not authenticated', async () => { 42 | const mockLocals = { 43 | authUser: null, 44 | } as App.Locals; 45 | const mockCookies = { 46 | get: vi.fn(), 47 | } as Partial; 48 | const mockServerLoadEvent = { 49 | locals: mockLocals, 50 | cookies: mockCookies, 51 | } as LayoutServerLoadEvent; 52 | 53 | const result = await load(mockServerLoadEvent); 54 | 55 | expect(result).toEqual({ 56 | authUser: null, 57 | }); 58 | expect(mockCookies.get).toHaveBeenCalledTimes(1); 59 | expect(mockCookies.get).toHaveBeenCalledWith('flash'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/routes/maintenance/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit'; 2 | 3 | import { config } from '$lib/server/core/config'; 4 | import { decodeOriginalPath } from '$lib/shared/core/utils'; 5 | 6 | import type { PageServerLoad } from './$types'; 7 | 8 | export const load = (({ url, setHeaders }) => { 9 | if (config.isMaintenanceMode) { 10 | setHeaders({ 11 | 'Retry-After': '600', 12 | }); 13 | throw error(503); 14 | } 15 | 16 | const originalPath = decodeOriginalPath(url); 17 | if (originalPath) { 18 | throw redirect(307, originalPath); 19 | } 20 | 21 | throw redirect(307, '/'); 22 | }) satisfies PageServerLoad; 23 | -------------------------------------------------------------------------------- /src/routes/sign-in/+page.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | Sign in{config.pageTitleSuffix} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |

Sign in

42 |
43 | 44 |
45 |
46 | 52 |
53 | 54 | {#if $message} 55 | 58 | {/if} 59 |
60 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /src/routes/sign-in/page.svelte.dom-test.ts: -------------------------------------------------------------------------------- 1 | import type { ToastStore } from '@skeletonlabs/skeleton'; 2 | import * as skeletonlabsSkeletonModule from '@skeletonlabs/skeleton'; 3 | import { 4 | cleanup, 5 | queries, 6 | render, 7 | type RenderResult, 8 | } from '@testing-library/svelte'; 9 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 10 | 11 | import { getMockFormValue } from '$lib/shared/superforms/testing'; 12 | 13 | import type { FormSchema } from './+page.server'; 14 | import Component from './+page.svelte'; 15 | 16 | describe(Component.name, () => { 17 | let renderResult: RenderResult; 18 | 19 | beforeEach(() => { 20 | vi.spyOn(skeletonlabsSkeletonModule, 'getToastStore').mockReturnValue({ 21 | trigger: vi.fn(), 22 | } as Partial as ToastStore); 23 | renderResult = render(Component, { 24 | props: { 25 | data: { 26 | authUser: null, 27 | form: getMockFormValue(), 28 | }, 29 | }, 30 | }); 31 | }); 32 | 33 | afterEach(() => { 34 | cleanup(); 35 | vi.restoreAllMocks(); 36 | }); 37 | 38 | it('should render the component', () => { 39 | expect(renderResult.component).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/styles/base.postcss: -------------------------------------------------------------------------------- 1 | @layer base { 2 | /* :root { 3 | --color-primary: #ff0000; 4 | } */ 5 | 6 | /* Open Sans */ 7 | @font-face { 8 | font-family: 'Open Sans'; 9 | src: url('/fonts/Open_Sans/static/OpenSans-Bold.ttf') format('truetype'); 10 | font-weight: bold; 11 | font-style: normal; 12 | font-display: swap; 13 | } 14 | @font-face { 15 | font-family: 'Open Sans'; 16 | src: url('/fonts/Open_Sans/static/OpenSans-Regular.ttf') format('truetype'); 17 | font-weight: normal; 18 | font-style: normal; 19 | font-display: swap; 20 | } 21 | 22 | /* Inter */ 23 | @font-face { 24 | font-family: 'Inter'; 25 | src: url('/fonts/Inter/static/Inter-Bold.ttf') format('truetype'); 26 | font-weight: bold; 27 | font-style: normal; 28 | font-display: swap; 29 | } 30 | @font-face { 31 | font-family: 'Inter'; 32 | src: url('/fonts/Inter/static/Inter-Regular.ttf') format('truetype'); 33 | font-weight: normal; 34 | font-style: normal; 35 | font-display: swap; 36 | } 37 | 38 | /* Source Code Pro */ 39 | @font-face { 40 | font-family: 'Source Code Pro'; 41 | src: url('/fonts/Source_Code_Pro/static/SourceCodePro-Bold.ttf') 42 | format('truetype'); 43 | font-weight: bold; 44 | font-style: normal; 45 | font-display: swap; 46 | } 47 | @font-face { 48 | font-family: 'Source Code Pro'; 49 | src: url('/fonts/Source_Code_Pro/static/SourceCodePro-Regular.ttf') 50 | format('truetype'); 51 | font-weight: normal; 52 | font-style: normal; 53 | font-display: swap; 54 | } 55 | 56 | /* Override Skeleton CSS variables for Wintry theme */ 57 | :root [data-theme='wintry'] { 58 | --theme-font-family-heading: Inter, system-ui, sans-serif; 59 | --theme-font-family-base: 'Open Sans', system-ui; 60 | } 61 | 62 | html, 63 | body { 64 | @apply m-0 65 | block 66 | h-full 67 | w-full 68 | overflow-hidden 69 | p-0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/favicon.png -------------------------------------------------------------------------------- /static/fonts/Inter/README.md: -------------------------------------------------------------------------------- 1 | Source: Google Fonts 2 | -------------------------------------------------------------------------------- /static/fonts/Inter/static/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/fonts/Inter/static/Inter-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/Inter/static/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/fonts/Inter/static/Inter-Regular.ttf -------------------------------------------------------------------------------- /static/fonts/Open_Sans/README.md: -------------------------------------------------------------------------------- 1 | Source: Google Fonts 2 | -------------------------------------------------------------------------------- /static/fonts/Open_Sans/static/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/fonts/Open_Sans/static/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/Open_Sans/static/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/fonts/Open_Sans/static/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /static/fonts/Source_Code_Pro/README.md: -------------------------------------------------------------------------------- 1 | Source: Google Fonts 2 | -------------------------------------------------------------------------------- /static/fonts/Source_Code_Pro/static/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/fonts/Source_Code_Pro/static/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/Source_Code_Pro/static/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/static/fonts/Source_Code_Pro/static/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: [vitePreprocess()], 9 | kit: { 10 | /** 11 | * `adapter-auto` only supports some environments, see 12 | * https://kit.svelte.dev/docs/adapter-auto for a list. 13 | * If your environment is not supported or you settled on a specific 14 | * environment, switch out the adapter. 15 | * See https://kit.svelte.dev/docs/adapters for more information about 16 | * adapters. 17 | */ 18 | adapter: adapter(), 19 | // csrf: { 20 | // checkOrigin: false, 21 | // }, 22 | }, 23 | vitePlugin: { 24 | inspector: true, 25 | }, 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /tests/playwright/api/tests/index.api-setup.ts: -------------------------------------------------------------------------------- 1 | import { test as setup } from '@playwright/test'; 2 | 3 | import { config } from '../../common/lib/config'; 4 | import { 5 | saveSignedInUserBeforeSignOutRoleState, 6 | saveSignedInUserRoleState, 7 | saveVisitorRoleState, 8 | seedDb, 9 | signIn, 10 | } from '../../common/lib/setup'; 11 | 12 | setup('setup API', async ({ page, baseURL }) => { 13 | if (!baseURL) { 14 | throw new Error('Base URL is not set'); 15 | } 16 | 17 | seedDb(); 18 | await saveVisitorRoleState(page); 19 | 20 | await signIn(page, baseURL, config.testData.authSessionCookie.value); 21 | await saveSignedInUserRoleState(page); 22 | 23 | await page.context().clearCookies(); 24 | await signIn( 25 | page, 26 | baseURL, 27 | config.testData.authSessionCookie.signOutTestsValue, 28 | ); 29 | await saveSignedInUserBeforeSignOutRoleState(page); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/playwright/api/tests/index.api-teardown.ts: -------------------------------------------------------------------------------- 1 | import { test as teardown } from '@playwright/test'; 2 | 3 | teardown('teardown API', () => { 4 | console.log('API teardown complete'); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/playwright/api/tests/signed-in-user/code-snippets/create.api-test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { HomePage } from '../../../../e2e/page-objects/pages/home.page'; 5 | 6 | const CODE_SNIPPET = config.testData.codeSnippets.forCreation; 7 | 8 | test.describe('POST /code-snippets/create?/create', () => { 9 | test('successfully creates a code snippet and redirects to home page', async ({ 10 | page, 11 | baseURL, 12 | }) => { 13 | const homePage = new HomePage(page); 14 | await homePage.doNavigateTo(); 15 | 16 | const response = await page.request.post('/code-snippets/create?/create', { 17 | headers: { 18 | // Needed to pass CSRF check 19 | origin: baseURL!, 20 | }, 21 | multipart: { 22 | name: CODE_SNIPPET.name, 23 | code: CODE_SNIPPET.code, 24 | }, 25 | }); 26 | 27 | expect(response.status()).toBe(200); 28 | expect(await response.json()).toEqual({ 29 | location: '/', 30 | status: 307, 31 | type: 'redirect', 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/playwright/api/tests/signed-in-user/code-snippets/delete.api-test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { HomePage } from '../../../../e2e/page-objects/pages/home.page'; 5 | 6 | const CODE_SNIPPET = config.testData.codeSnippets.forDeletion; 7 | 8 | test.describe('POST /code-snippets/:id?/delete', () => { 9 | test('successfully deletes a code snippet and redirects to root path', async ({ 10 | page, 11 | baseURL, 12 | }) => { 13 | const homePage = new HomePage(page); 14 | await homePage.doNavigateTo(); 15 | 16 | const response = await page.request.post( 17 | `/code-snippets/${CODE_SNIPPET.id}?/delete`, 18 | { 19 | headers: { 20 | // Needed to pass CSRF check 21 | origin: baseURL!, 22 | }, 23 | multipart: {}, 24 | }, 25 | ); 26 | 27 | expect(response.status()).toBe(200); 28 | expect(await response.json()).toEqual({ 29 | location: '/', 30 | status: 307, 31 | type: 'redirect', 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/playwright/api/tests/signed-in-user/code-snippets/edit.api-test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { HomePage } from '../../../../e2e/page-objects/pages/home.page'; 5 | 6 | const CODE_SNIPPET = config.testData.codeSnippets.forEditing; 7 | 8 | test.describe('POST /code-snippets/:id/edit?/edit', () => { 9 | test('successfully edits a code snippet and returns a message', async ({ 10 | page, 11 | baseURL, 12 | }) => { 13 | const homePage = new HomePage(page); 14 | await homePage.doNavigateTo(); 15 | 16 | const response = await page.request.post( 17 | `/code-snippets/${CODE_SNIPPET.id}/edit?/edit`, 18 | { 19 | headers: { 20 | // Needed to pass CSRF check 21 | origin: baseURL!, 22 | }, 23 | multipart: { 24 | name: CODE_SNIPPET.newName, 25 | code: CODE_SNIPPET.code, 26 | }, 27 | }, 28 | ); 29 | 30 | expect(response.status()).toBe(200); 31 | expect(await response.json()).toEqual({ 32 | data: `[{"form":1},{"id":2,"valid":3,"posted":3,"errors":4,"data":5,"constraints":8,"message":12},"erwwt8",true,{},{"name":6,"code":7},"${CODE_SNIPPET.newName}","${CODE_SNIPPET.code}",{"name":9,"code":11},{"minlength":10,"required":3},1,{"minlength":10,"required":3},{"type":13,"message":14},"success","Code snippet edited"]`, 33 | status: 200, 34 | type: 'success', 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/playwright/api/tests/visitor/sign-in.api-test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('POST /sign-in?/google-auth', () => { 4 | test('successfully redirects to /auth/google page', async ({ 5 | request, 6 | baseURL, 7 | }, testInfo) => { 8 | const response = await request.post('/sign-in?/google-auth', { 9 | headers: { 10 | // Needed to pass CSRF check 11 | origin: baseURL!, 12 | }, 13 | form: {}, 14 | }); 15 | 16 | expect(response.status()).toBe(200); 17 | const json = await response.json(); 18 | const locationUrl = new URL(json.location); 19 | expect(locationUrl.origin).toBe('https://accounts.google.com'); 20 | // `searchParams.get` method automatically decodes given query param value 21 | expect(locationUrl.searchParams.get('redirect_uri')).toBe( 22 | `${testInfo.project.use.baseURL}/sign-in?oauth-type=google`, 23 | ); 24 | expect(await response.json()).toEqual({ 25 | location: expect.any(String), 26 | status: 307, 27 | type: 'redirect', 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/playwright/common/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SESSION_COOKIE_NAME } from 'lucia'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | export const config = { 10 | testData: { 11 | authSessionCookie: { 12 | name: DEFAULT_SESSION_COOKIE_NAME, 13 | value: '5x39nr9tnlb4pzj3fd7t1sz93n6azqq392dluyft', 14 | signOutTestsValue: '9x39nr9tnlb4pzj3fd7t1sz93n6azqq392dluyft', 15 | }, 16 | codeSnippets: { 17 | forCreation: { 18 | name: 'Created', 19 | code: "console.log('Created');", 20 | }, 21 | forEditing: { 22 | id: 1, 23 | name: 'For editing', 24 | newName: 'For editing - Edited', 25 | code: "console.log('For editing');", 26 | }, 27 | forDeletion: { 28 | id: 2, 29 | name: 'For deletion', 30 | code: "console.log('For deletion');", 31 | }, 32 | forViewing: { 33 | id: 3, 34 | name: 'For viewing', 35 | code: "console.log('For viewing');", 36 | }, 37 | fromOtherUser: { 38 | id: 4, 39 | name: 'From other user', 40 | code: "console.log('From other user');", 41 | }, 42 | forPaginationCheck: { 43 | name: 'For pagination check', 44 | code: "console.log('For pagination check');", 45 | }, 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /tests/playwright/common/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import url from 'url'; 3 | 4 | const __filename = url.fileURLToPath(import.meta.url); 5 | const __dirname = path.dirname(__filename); 6 | 7 | export const COMMON_TESTS_FOLDER = path.join( 8 | __dirname, 9 | '..', 10 | '..', 11 | 'common', 12 | 'tests', 13 | ); 14 | export const COMMON_REPORTS_FOLDER = path.join( 15 | __dirname, 16 | '..', 17 | '..', 18 | '..', 19 | '..', 20 | 'reports', 21 | 'playwright', 22 | 'common', 23 | ); 24 | export const COMMON_SAVED_STATES_FOLDER = path.join( 25 | __dirname, 26 | '..', 27 | 'saved-states', 28 | ); 29 | 30 | export const E2E_TESTS_FOLDER = path.join( 31 | __dirname, 32 | '..', 33 | '..', 34 | 'e2e', 35 | 'tests', 36 | ); 37 | export const E2E_REPORTS_FOLDER = path.join( 38 | __dirname, 39 | '..', 40 | '..', 41 | '..', 42 | '..', 43 | 'reports', 44 | 'playwright', 45 | 'e2e', 46 | ); 47 | 48 | export const API_TESTS_FOLDER = path.join( 49 | __dirname, 50 | '..', 51 | '..', 52 | 'api', 53 | 'tests', 54 | ); 55 | export const API_REPORTS_FOLDER = path.join( 56 | __dirname, 57 | '..', 58 | '..', 59 | '..', 60 | '..', 61 | 'reports', 62 | 'playwright', 63 | 'api', 64 | ); 65 | -------------------------------------------------------------------------------- /tests/playwright/common/saved-states/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeexx/code-snippet-sharing/a7d68b01540e2a0f90f68c9c82bcf363a2506381/tests/playwright/common/saved-states/.gitkeep -------------------------------------------------------------------------------- /tests/playwright/e2e/page-objects/components/navigation-bar.component.ts: -------------------------------------------------------------------------------- 1 | import type { Locator, Page } from '@playwright/test'; 2 | 3 | export class NavigationBarComponent { 4 | constructor(public page: Page) {} 5 | 6 | public getSignInButton(): Locator { 7 | return this.getButtonWithText('Sign in'); 8 | } 9 | 10 | public getProfileButton(): Locator { 11 | return this.getButtonWithText('Profile'); 12 | } 13 | 14 | public doClickSignInButton(): Promise { 15 | return this.getSignInButton().click(); 16 | } 17 | 18 | public async doClickProfileButton(): Promise { 19 | await this.getProfileButton().click(); 20 | } 21 | 22 | private getButtonWithText(text: string): Locator { 23 | return this.page.getByRole('button', { name: text }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/playwright/e2e/page-objects/pages/profile.page.ts: -------------------------------------------------------------------------------- 1 | import { expect, type Locator, type Page } from '@playwright/test'; 2 | 3 | export class ProfilePage { 4 | urlPath = '/profile'; 5 | urlPathRegex = RegExp(`${this.urlPath}[^/]*`); 6 | 7 | constructor(public page: Page) {} 8 | 9 | public getTitle(): Promise { 10 | return this.page.title(); 11 | } 12 | 13 | public async doNavigateTo(): Promise { 14 | await this.page.goto(this.urlPath, { waitUntil: 'domcontentloaded' }); 15 | // Vite dev sometimes needs more time to connect to page 16 | await this.page.waitForTimeout(1000); 17 | } 18 | 19 | public async doWaitForURL(): Promise { 20 | await this.page.waitForURL(this.urlPathRegex); 21 | } 22 | 23 | public getSignOutButton(): Locator { 24 | return this.getButtonWithText('Sign out'); 25 | } 26 | 27 | public doClickSignOutButton(): Promise { 28 | return this.getSignOutButton().click(); 29 | } 30 | 31 | public async checkTitle(title: string): Promise { 32 | await expect(this.page).toHaveTitle(title); 33 | } 34 | 35 | private getButtonWithText(text: string): Locator { 36 | return this.page.getByRole('button', { name: text }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/playwright/e2e/page-objects/pages/sign-in.page.ts: -------------------------------------------------------------------------------- 1 | import { expect, type Locator, type Page } from '@playwright/test'; 2 | 3 | export class SignInPage { 4 | urlPath = '/sign-in'; 5 | urlPathRegex = RegExp(`${this.urlPath}[^/]*`); 6 | 7 | constructor(public page: Page) {} 8 | 9 | public getTitle(): Promise { 10 | return this.page.title(); 11 | } 12 | 13 | public async doNavigateTo(): Promise { 14 | await this.page.goto(this.urlPath, { waitUntil: 'domcontentloaded' }); 15 | // Vite dev sometimes needs more time to connect to page 16 | await this.page.waitForTimeout(1000); 17 | } 18 | 19 | public async doWaitForURL(): Promise { 20 | await this.page.waitForURL(this.urlPathRegex); 21 | } 22 | 23 | public getSignInWithGoogleButton(): Locator { 24 | return this.getButtonWithText('Sign in with Google'); 25 | } 26 | 27 | public doClickSignInWithGoogleButton(): Promise { 28 | return this.getSignInWithGoogleButton().click(); 29 | } 30 | 31 | public async checkTitle(title: string): Promise { 32 | await expect(this.page).toHaveTitle(title); 33 | } 34 | 35 | private getButtonWithText(text: string): Locator { 36 | return this.page.getByRole('button', { name: text }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/index.e2e-setup.ts: -------------------------------------------------------------------------------- 1 | import { test as setup } from '@playwright/test'; 2 | 3 | import { config } from '../../common/lib/config'; 4 | import { 5 | saveSignedInUserBeforeSignOutRoleState, 6 | saveSignedInUserRoleState, 7 | saveVisitorRoleState, 8 | seedDb, 9 | signIn, 10 | } from '../../common/lib/setup'; 11 | 12 | setup('setup E2E', async ({ page, baseURL }) => { 13 | seedDb(); 14 | await saveVisitorRoleState(page); 15 | 16 | await signIn(page, baseURL, config.testData.authSessionCookie.value); 17 | await saveSignedInUserRoleState(page); 18 | 19 | await page.context().clearCookies(); 20 | await signIn( 21 | page, 22 | baseURL, 23 | config.testData.authSessionCookie.signOutTestsValue, 24 | ); 25 | await saveSignedInUserBeforeSignOutRoleState(page); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/index.e2e-teardown.ts: -------------------------------------------------------------------------------- 1 | import { test as teardown } from '@playwright/test'; 2 | 3 | teardown('teardown E2E', () => { 4 | console.log('E2E teardown complete'); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/signed-in-user--before-sign-out/sign-out.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { HomePage } from '../../page-objects/pages/home.page.js'; 4 | import { ProfilePage } from '../../page-objects/pages/profile.page.js'; 5 | 6 | test.describe('Feature: Sign out', () => { 7 | test('Example: Where Signed in user successfully signs out', async ({ 8 | page, 9 | }) => { 10 | const homePage = new HomePage(page); 11 | await homePage.doNavigateTo(); 12 | 13 | await expect( 14 | homePage.componentNavigationBar.getSignInButton(), 15 | ).toBeHidden(); 16 | await expect( 17 | homePage.componentNavigationBar.getProfileButton(), 18 | ).toBeVisible(); 19 | 20 | await homePage.componentNavigationBar.doClickProfileButton(); 21 | const profilePage = new ProfilePage(page); 22 | await profilePage.doWaitForURL(); 23 | 24 | await profilePage.doClickSignOutButton(); 25 | await homePage.doWaitForURL(); 26 | 27 | await expect( 28 | homePage.componentNavigationBar.getSignInButton(), 29 | ).toBeVisible(); 30 | await expect( 31 | homePage.componentNavigationBar.getProfileButton(), 32 | ).toBeHidden(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/signed-in-user/code-snippets/create.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { CodeSnippetCreatePage } from '../../../page-objects/pages/code-snippets/create.page.js'; 5 | import { HomePage } from '../../../page-objects/pages/home.page.js'; 6 | 7 | const CODE_SNIPPET = config.testData.codeSnippets.forCreation; 8 | 9 | test.describe('Feature: Create a code snippet', () => { 10 | test('Example: Where Signed in user successfully creates a Code snippet', async ({ 11 | page, 12 | }) => { 13 | const codeSnippetCreatePage = new CodeSnippetCreatePage(page); 14 | await codeSnippetCreatePage.doNavigateTo(); 15 | 16 | await codeSnippetCreatePage.doClickNameInput(); 17 | await codeSnippetCreatePage.doFillNameInput(CODE_SNIPPET.name); 18 | await codeSnippetCreatePage.doClickCodeInput(); 19 | await codeSnippetCreatePage.doFillCodeInput(CODE_SNIPPET.code); 20 | await codeSnippetCreatePage.doClickConfirmButton(); 21 | 22 | const homePage = new HomePage(page); 23 | await homePage.doWaitForURL(); 24 | await homePage.doSelectCreationDateSortingDropdownOption('desc'); 25 | await homePage.checkTextVisible(CODE_SNIPPET.name); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/signed-in-user/code-snippets/delete.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { CodeSnippetViewDetailsPage } from '../../../page-objects/pages/code-snippets/view-details.page'; 5 | import { HomePage } from '../../../page-objects/pages/home.page'; 6 | 7 | const CODE_SNIPPET = config.testData.codeSnippets.forDeletion; 8 | 9 | test.describe('Feature: Delete a code snippet', () => { 10 | test('Example: Where Signed in user successfully deletes a Code snippet', async ({ 11 | page, 12 | }) => { 13 | const homePage = new HomePage(page); 14 | await homePage.doNavigateTo(); 15 | await homePage.checkTextVisible(CODE_SNIPPET.name); 16 | 17 | const codeSnippetViewDetailsPage = new CodeSnippetViewDetailsPage( 18 | page, 19 | CODE_SNIPPET.id, 20 | ); 21 | await codeSnippetViewDetailsPage.doNavigateTo(); 22 | await codeSnippetViewDetailsPage.doClickDeleteButton(); 23 | 24 | await homePage.doNavigateTo(); 25 | await homePage.checkTextHidden(CODE_SNIPPET.name); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/signed-in-user/code-snippets/edit.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { CodeSnippetEditPage } from '../../../page-objects/pages/code-snippets/edit.page.js'; 5 | import { HomePage } from '../../../page-objects/pages/home.page.js'; 6 | 7 | const CODE_SNIPPET = config.testData.codeSnippets.forEditing; 8 | 9 | test.describe('Feature: Edit a code snippet', () => { 10 | test('Example: Where Signed in user successfully edits a Code snippet', async ({ 11 | page, 12 | }) => { 13 | const homePage = new HomePage(page); 14 | await homePage.doNavigateTo(); 15 | await homePage.checkTextVisible(CODE_SNIPPET.name); 16 | await homePage.checkTextHidden(CODE_SNIPPET.newName); 17 | 18 | const codeSnippetCreatePage = new CodeSnippetEditPage( 19 | page, 20 | CODE_SNIPPET.id, 21 | ); 22 | await codeSnippetCreatePage.doNavigateTo(); 23 | 24 | await codeSnippetCreatePage.doClickNameInput(); 25 | await codeSnippetCreatePage.doFillNameInput(CODE_SNIPPET.newName); 26 | await codeSnippetCreatePage.doClickConfirmButton(); 27 | 28 | await homePage.doNavigateTo(); 29 | await homePage.checkTextHidden(CODE_SNIPPET.name); 30 | await homePage.checkTextVisible(CODE_SNIPPET.newName); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/signed-in-user/code-snippets/view-details.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { CodeSnippetViewDetailsPage } from '../../../page-objects/pages/code-snippets/view-details.page'; 5 | 6 | const CODE_SNIPPET = config.testData.codeSnippets.forViewing; 7 | 8 | test.describe("Feature: View code snippet's details", () => { 9 | test("Example: Where Signed in user successfully views Code snippet's details", async ({ 10 | page, 11 | }) => { 12 | const codeSnippetsViewDetailsPage = new CodeSnippetViewDetailsPage( 13 | page, 14 | CODE_SNIPPET.id, 15 | ); 16 | await codeSnippetsViewDetailsPage.doNavigateTo(); 17 | 18 | await codeSnippetsViewDetailsPage.checkCodeSnippetName(CODE_SNIPPET.name); 19 | await codeSnippetsViewDetailsPage.checkCodeSnippetCode(CODE_SNIPPET.code); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/visitor/code-snippets/view-details.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { CodeSnippetViewDetailsPage } from '../../../page-objects/pages/code-snippets/view-details.page'; 5 | 6 | const CODE_SNIPPET = config.testData.codeSnippets.forViewing; 7 | 8 | test.describe("Feature: View code snippet's details", () => { 9 | test("Example: Where Visitor successfully views Code snippet's details", async ({ 10 | page, 11 | }) => { 12 | const codeSnippetViewDetailsPage = new CodeSnippetViewDetailsPage( 13 | page, 14 | CODE_SNIPPET.id, 15 | ); 16 | await codeSnippetViewDetailsPage.doNavigateTo(); 17 | 18 | await codeSnippetViewDetailsPage.checkCodeSnippetName(CODE_SNIPPET.name); 19 | await codeSnippetViewDetailsPage.checkCodeSnippetCode(CODE_SNIPPET.code); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/visitor/code-snippets/view.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | import { config } from '../../../../common/lib/config'; 4 | import { HomePage } from '../../../page-objects/pages/home.page'; 5 | 6 | const FIRST_CODE_SNIPPET = config.testData.codeSnippets.forViewing; 7 | const LAST_CODE_SNIPPET = config.testData.codeSnippets.forPaginationCheck; 8 | 9 | test.describe('Feature: View all code snippets', () => { 10 | test('Example: Where Visitor successfully accesses a list of all Code snippets', async ({ 11 | page, 12 | }) => { 13 | const homePage = new HomePage(page); 14 | await homePage.doNavigateTo(); 15 | 16 | await homePage.checkTextVisible(FIRST_CODE_SNIPPET.name); 17 | await homePage.checkTextHidden(LAST_CODE_SNIPPET.name); 18 | 19 | await homePage.doClickNextPageButton(); 20 | 21 | await homePage.checkTextHidden(FIRST_CODE_SNIPPET.name); 22 | await homePage.checkTextVisible(LAST_CODE_SNIPPET.name); 23 | }); 24 | 25 | test('Example: Where Visitor successfully accesses a list of all Code snippets in descending order based on their creation dates', async ({ 26 | page, 27 | }) => { 28 | const homePage = new HomePage(page); 29 | await homePage.doNavigateTo(); 30 | 31 | await homePage.checkTextVisible(FIRST_CODE_SNIPPET.name); 32 | await homePage.checkTextHidden(LAST_CODE_SNIPPET.name); 33 | 34 | await homePage.doSelectCreationDateSortingDropdownOption('desc'); 35 | 36 | await homePage.checkTextHidden(FIRST_CODE_SNIPPET.name); 37 | await homePage.checkTextVisible(LAST_CODE_SNIPPET.name); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/playwright/e2e/tests/visitor/sign-in.e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { HomePage } from '../../page-objects/pages/home.page.js'; 4 | import { SignInPage } from '../../page-objects/pages/sign-in.page.js'; 5 | 6 | test.describe('Feature: Sign in/Sign up via Google', () => { 7 | test.describe('Example: Where Visitor successfully signs in using his/her Google account', () => { 8 | test('Part: Until Google Sign In page', async ({ page }) => { 9 | const homePage = new HomePage(page); 10 | await homePage.doNavigateTo(); 11 | 12 | await expect( 13 | homePage.componentNavigationBar.getSignInButton(), 14 | ).toBeVisible(); 15 | await expect( 16 | homePage.componentNavigationBar.getProfileButton(), 17 | ).toBeHidden(); 18 | 19 | await homePage.componentNavigationBar.doClickSignInButton(); 20 | 21 | const signInPage = new SignInPage(page); 22 | await signInPage.doWaitForURL(); 23 | 24 | await signInPage.doClickSignInWithGoogleButton(); 25 | await page.waitForURL(/^https:\/\/accounts.google.com/); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /vitest.browser.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used for running app's browser tests of client code. 3 | */ 4 | 5 | import { defineConfig, mergeConfig } from 'vitest/config'; 6 | 7 | import commonConfig from './vitest.common.config'; 8 | 9 | export default defineConfig((configEnv) => 10 | mergeConfig( 11 | commonConfig(configEnv), 12 | defineConfig({ 13 | /* https://github.com/vitest-dev/vitest/issues/3286 */ 14 | // optimizeDeps: { 15 | // exclude: [ 16 | // 'vitest', 17 | // 'vitest/utils', 18 | // 'vitest/browser', 19 | // 'vitest/runners', 20 | // '@vitest/utils', 21 | // ], 22 | // include: [ 23 | // '@vitest/utils > concordance', 24 | // '@vitest/utils > loupe', 25 | // '@vitest/utils > pretty-format', 26 | // 'vitest > chai', 27 | // ], 28 | // }, 29 | test: { 30 | include: ['src/**/*.browser-test.{js,ts}'], 31 | browser: { 32 | enabled: true, 33 | name: 'chromium', 34 | headless: true, 35 | provider: 'playwright', 36 | }, 37 | coverage: { 38 | reportsDirectory: './reports/vitest/coverage/app/browser', 39 | }, 40 | }, 41 | }), 42 | ), 43 | ); 44 | -------------------------------------------------------------------------------- /vitest.common.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | 3 | import viteConfig from './vite.config'; 4 | 5 | export default defineConfig((configEnv) => 6 | mergeConfig( 7 | viteConfig(configEnv), 8 | defineConfig({ 9 | test: { 10 | coverage: { 11 | provider: 'v8', 12 | reporter: ['text', 'html'], 13 | include: ['src/**/*.{js,ts,svelte}'], 14 | all: true, 15 | reportsDirectory: './reports/vitest/coverage/unknown', 16 | }, 17 | }, 18 | }), 19 | ), 20 | ); 21 | -------------------------------------------------------------------------------- /vitest.dom-node.setup.ts: -------------------------------------------------------------------------------- 1 | import { SveltekitDefaultMocks } from '$lib/shared/sveltekit/testing'; 2 | 3 | SveltekitDefaultMocks.applyDefaultMocks(); 4 | -------------------------------------------------------------------------------- /vitest.dom.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used for app's unit tests of client code using Node.js and JSDOM. 3 | */ 4 | 5 | import { defineConfig, mergeConfig } from 'vitest/config'; 6 | 7 | import commonConfig from './vitest.common.config'; 8 | 9 | export default defineConfig((configEnv) => 10 | mergeConfig( 11 | commonConfig(configEnv), 12 | defineConfig({ 13 | test: { 14 | environment: 'jsdom', 15 | include: ['src/**/*.dom-test.{js,ts}'], 16 | setupFiles: ['./vitest.dom.setup.ts'], 17 | coverage: { 18 | reportsDirectory: './reports/vitest/coverage/app/dom', 19 | }, 20 | // Needed for `onMount` to work in tests 21 | // https://github.com/vitest-dev/vitest/issues/2834 22 | alias: [{ find: /^svelte$/, replacement: 'svelte/internal' }], 23 | }, 24 | }), 25 | ), 26 | ); 27 | -------------------------------------------------------------------------------- /vitest.dom.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import './vitest.dom-node.setup'; 3 | -------------------------------------------------------------------------------- /vitest.node.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used for app's unit tests of server code. 3 | */ 4 | 5 | import { defineConfig, mergeConfig } from 'vitest/config'; 6 | 7 | import commonConfig from './vitest.common.config'; 8 | 9 | export default defineConfig((configEnv) => 10 | mergeConfig( 11 | commonConfig(configEnv), 12 | defineConfig({ 13 | test: { 14 | environment: 'node', 15 | include: ['src/**/*.node-test.{js,ts}'], 16 | setupFiles: ['./vitest.node.setup.ts'], 17 | coverage: { 18 | reportsDirectory: './reports/vitest/coverage/app/node', 19 | }, 20 | }, 21 | }), 22 | ), 23 | ); 24 | -------------------------------------------------------------------------------- /vitest.node.setup.ts: -------------------------------------------------------------------------------- 1 | import './vitest.dom-node.setup'; 2 | -------------------------------------------------------------------------------- /wallaby.dom.template.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * How to use: 3 | * EITHER Ctrl+Shift+P -> Wallaby.js: Select Configuration 4 | * OR Copy as wallaby.cjs to project root to test 5 | * using vitest.dom.config.ts. 6 | */ 7 | 8 | module.exports = function () { 9 | return { 10 | testFramework: { 11 | configFile: './vitest.dom.config.ts', 12 | }, 13 | hints: { 14 | ignoreCoverageForFile: /Wallaby ignore file coverage/, 15 | ignoreCoverage: /Wallaby ignore coverage/, 16 | }, 17 | trace: true, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /wallaby.node.template.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * How to use: 3 | * EITHER Ctrl+Shift+P -> Wallaby.js: Select Configuration 4 | * OR Copy as wallaby.cjs to project root to test 5 | * using vitest.node.config.ts. 6 | */ 7 | 8 | module.exports = function () { 9 | return { 10 | testFramework: { 11 | configFile: './vitest.node.config.ts', 12 | }, 13 | hints: { 14 | ignoreCoverageForFile: /Wallaby ignore file coverage/, 15 | ignoreCoverage: /Wallaby ignore coverage/, 16 | }, 17 | trace: true, 18 | }; 19 | }; 20 | --------------------------------------------------------------------------------