diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 51292e6..c2c2199 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,8 +34,7 @@ jobs: # Install dependencies - name: ๐Ÿ“ฆ Install dependencies - run: | - npm ci + run: npm ci # Lint code - name: ๐Ÿงน Lint code @@ -44,3 +43,23 @@ jobs: # Run tests - name: ๐Ÿงช Run tests run: npm test + + # Setup PocketBase + - name: ๐Ÿ—„๏ธ Download and setup PocketBase + run: npm run test:e2e:setup + + # Start PocketBase + - name: ๐Ÿš€ Start PocketBase + run: ./.pocketbase/pocketbase serve & + + # Wait for PocketBase to be ready + - name: โณ Wait for PocketBase + run: | + until curl -s --fail http://localhost:8090/api/health; do + echo 'Waiting for PocketBase...' + sleep 5 + done + + # Run tests + - name: ๐Ÿงช Run e2e tests + run: npm run test:e2e diff --git a/.gitignore b/.gitignore index 1763804..0846ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # build output dist/ coverage/ +.eslintcache # generated types .astro/ @@ -13,7 +14,6 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* - # environment variables .env .env.production @@ -23,3 +23,6 @@ pnpm-debug.log* # jetbrains setting folder .idea/ + +# PocketBase folder +.pocketbase/ \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 718c0b3..4ba64b2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ import tseslint from "typescript-eslint"; const config = tseslint.config({ files: ["**/*.{js,mjs,cjs,ts}"], + ignores: [".pocketbase/**/*"], languageOptions: { globals: { ...globals.browser, ...globals.node } }, diff --git a/package.json b/package.json index 0786796..23fe77a 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,16 @@ "src" ], "scripts": { - "lint": "npx eslint", + "lint": "npx eslint --cache", "prepare": "husky", "test": "vitest run", "test:watch": "vitest watch", "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "test:e2e": "vitest run --config vitest.config-e2e.ts", + "test:e2e:watch": "vitest watch --config vitest.config-e2e.ts", + "test:e2e:setup": "./scripts/setup-pocketbase.sh", + "test:e2e:pocketbase": "./.pocketbase/pocketbase serve" }, "peerDependencies": { "astro": "^5.0.0" diff --git a/scripts/setup-pocketbase.sh b/scripts/setup-pocketbase.sh new file mode 100755 index 0000000..ee7c61b --- /dev/null +++ b/scripts/setup-pocketbase.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Define the local .pocketbase directory +POCKETBASE_DIR="$(pwd)/.pocketbase" + +# Remove the existing .pocketbase directory if it exists +rm -rf "$POCKETBASE_DIR" + +# Create the .pocketbase directory if it doesn't exist +mkdir -p "$POCKETBASE_DIR" + +# Change to the .pocketbase directory +cd "$POCKETBASE_DIR" + +# Get the latest release tag from PocketBase GitHub releases +latest_release=$(curl -s https://api.github.com/repos/pocketbase/pocketbase/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")') + +# Determine the architecture +arch=$(uname -m) +if [ "$arch" == "x86_64" ]; then + arch="amd64" +elif [ "$arch" == "aarch64" ]; then + arch="arm64" +else + echo "Unsupported architecture: $arch" + exit 1 +fi + +# Construct the download URL +download_url="https://github.com/pocketbase/pocketbase/releases/download/${latest_release}/pocketbase_${latest_release#v}_linux_${arch}.zip" + +# Download the latest release +echo "Downloading PocketBase ${latest_release}..." +curl -s -L -o pocketbase.zip $download_url + +# Check if the download was successful +if [ $? -ne 0 ]; then + echo "Failed to download PocketBase release." + exit 1 +fi + +# Extract the executable from the zip file +echo "Extracting PocketBase ${latest_release}..." +unzip -qq pocketbase.zip +if [ $? -ne 0 ]; then + echo "Failed to unzip PocketBase release." + exit 1 +fi + +# Make the executable file executable +chmod +x pocketbase + +# Clean up +rm pocketbase.zip + +# Setup admin user +echo "Setting up admin user..." +./pocketbase superuser upsert test@pawcode.de test1234 +if [ $? -ne 0 ]; then + echo "Failed to setup admin user." + exit 1 +fi + +echo "PocketBase ${latest_release} has been downloaded and is ready to use." diff --git a/test/_mocks/check-e2e-connection.ts b/test/_mocks/check-e2e-connection.ts new file mode 100644 index 0000000..ac4a364 --- /dev/null +++ b/test/_mocks/check-e2e-connection.ts @@ -0,0 +1,9 @@ +export async function checkE2eConnection(): Promise { + try { + await fetch("http://localhost:8090/api/health"); + } catch { + throw new Error( + "E2E connection failed. Make sure the PocketBase instance is running on http://localhost:8090." + ); + } +} diff --git a/test/_mocks/create-loader-options.ts b/test/_mocks/create-loader-options.ts index 26d5a1c..583c84a 100644 --- a/test/_mocks/create-loader-options.ts +++ b/test/_mocks/create-loader-options.ts @@ -4,8 +4,12 @@ export function createLoaderOptions( options?: Partial ): PocketBaseLoaderOptions { return { - url: "https://example.com", + url: "http://127.0.0.1:8090", collectionName: "test", + superuserCredentials: { + email: "test@pawcode.de", + password: "test1234" + }, ...options }; } diff --git a/test/_mocks/index.ts b/test/_mocks/index.ts index 775f68c..a193ae3 100644 --- a/test/_mocks/index.ts +++ b/test/_mocks/index.ts @@ -1,3 +1,4 @@ +export * from "./check-e2e-connection"; export * from "./create-loader-context"; export * from "./create-loader-options"; export * from "./create-pocketbase-entry"; diff --git a/test/utils/__snapshots__/get-remote-schema.spec-e2e.ts.snap b/test/utils/__snapshots__/get-remote-schema.spec-e2e.ts.snap new file mode 100644 index 0000000..0222950 --- /dev/null +++ b/test/utils/__snapshots__/get-remote-schema.spec-e2e.ts.snap @@ -0,0 +1,173 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`getRemoteSchema > should return schema if fetch request is successful 1`] = ` +{ + "authRule": "", + "authToken": { + "duration": 604800, + }, + "createRule": "", + "deleteRule": "id = @request.auth.id", + "emailChangeToken": { + "duration": 1800, + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text", + }, + { + "cost": 0, + "hidden": true, + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password", + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text", + }, + { + "exceptDomains": null, + "hidden": false, + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email", + }, + { + "hidden": false, + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool", + }, + { + "hidden": false, + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool", + }, + { + "autogeneratePattern": "", + "hidden": false, + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text", + }, + { + "hidden": false, + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp", + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file", + }, + { + "hidden": false, + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate", + }, + { + "hidden": false, + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate", + }, + ], + "fileToken": { + "duration": 180, + }, + "id": "_pb_users_auth_", + "indexes": [ + "CREATE UNIQUE INDEX \`idx_tokenKey__pb_users_auth_\` ON \`users\` (\`tokenKey\`)", + "CREATE UNIQUE INDEX \`idx_email__pb_users_auth_\` ON \`users\` (\`email\`) WHERE \`email\` != ''", + ], + "listRule": "id = @request.auth.id", + "manageRule": null, + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "", + }, + "name": "users", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "avatar", + "id": "", + "name": "name", + "username": "", + }, + "providers": [], + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email", + ], + }, + "passwordResetToken": { + "duration": 1800, + }, + "system": false, + "type": "auth", + "updateRule": "id = @request.auth.id", + "verificationToken": { + "duration": 259200, + }, + "viewRule": "id = @request.auth.id", +} +`; diff --git a/test/utils/get-remote-schema.spec-e2e.ts b/test/utils/get-remote-schema.spec-e2e.ts new file mode 100644 index 0000000..3ea0cbb --- /dev/null +++ b/test/utils/get-remote-schema.spec-e2e.ts @@ -0,0 +1,68 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { getRemoteSchema } from "../../src/utils/get-remote-schema"; +import { checkE2eConnection, createLoaderOptions } from "../_mocks"; + +describe("getRemoteSchema", () => { + const options = createLoaderOptions(); + + beforeAll(async () => { + await checkE2eConnection(); + }); + + it("should return undefined if no superuser credentials are provided", async () => { + const result = await getRemoteSchema({ + ...options, + superuserCredentials: undefined + }); + + expect(result).toBeUndefined(); + }); + + it("should return undefined if superuser token is invalid", async () => { + const result = await getRemoteSchema({ + ...options, + superuserCredentials: { email: "invalid", password: "invalid" } + }); + + expect(result).toBeUndefined(); + }); + + it("should return undefined if fetch request fails", async () => { + const result = await getRemoteSchema(options); + + expect(result).toBeUndefined(); + }); + + it("should return schema if fetch request is successful", async () => { + const result = await getRemoteSchema({ + ...options, + collectionName: "users" + }); + + expect(result).toBeDefined(); + + // Delete unstable properties + // @ts-expect-error - created is not typed + delete result.created; + // @ts-expect-error - updated is not typed + delete result.updated; + for (const field of result!.fields) { + // @ts-expect-error - id is not typed + delete field.id; + } + + // Delete email templates + // @ts-expect-error - authAlert is not typed + delete result.authAlert; + // @ts-expect-error - otp is not typed + delete result.otp; + // @ts-expect-error - verificationTemplate is not typed + delete result.verificationTemplate; + // @ts-expect-error - resetPasswordTemplate is not typed + delete result.resetPasswordTemplate; + // @ts-expect-error - confirmEmailChangeTemplate is not typed + delete result.confirmEmailChangeTemplate; + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/test/utils/get-superuser-token.spec-e2e.ts b/test/utils/get-superuser-token.spec-e2e.ts new file mode 100644 index 0000000..0662fb6 --- /dev/null +++ b/test/utils/get-superuser-token.spec-e2e.ts @@ -0,0 +1,27 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { getSuperuserToken } from "../../src/utils/get-superuser-token"; +import { checkE2eConnection, createLoaderOptions } from "../_mocks"; + +describe("getSuperuserToken", () => { + const options = createLoaderOptions(); + + beforeAll(async () => { + await checkE2eConnection(); + }); + + it("should return undefined if superuser credentials are invalid", async () => { + const result = await getSuperuserToken(options.url, { + email: "invalid", + password: "invalid" + }); + expect(result).toBeUndefined(); + }); + + it("should return token if fetch request is successful", async () => { + const result = await getSuperuserToken( + options.url, + options.superuserCredentials! + ); + expect(result).toBeDefined(); + }); +}); diff --git a/test/utils/read-local-schema.spec.ts b/test/utils/read-local-schema.spec.ts index b143d01..d731ed7 100644 --- a/test/utils/read-local-schema.spec.ts +++ b/test/utils/read-local-schema.spec.ts @@ -34,7 +34,6 @@ describe("readLocalSchema", () => { test("should return undefined if the collection is not found", async () => { vi.spyOn(path, "join").mockReturnValue(localSchemaPath); vi.spyOn(fs, "readFile").mockResolvedValue(JSON.stringify(mockSchema)); - vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readLocalSchema(localSchemaPath, "nonexistent"); expect(result).toBeUndefined(); @@ -43,7 +42,6 @@ describe("readLocalSchema", () => { test("should return undefined if the schema file is invalid", async () => { vi.spyOn(path, "join").mockReturnValue(localSchemaPath); vi.spyOn(fs, "readFile").mockResolvedValue("invalid json"); - vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readLocalSchema(localSchemaPath, collectionName); expect(result).toBeUndefined(); @@ -53,8 +51,6 @@ describe("readLocalSchema", () => { vi.spyOn(path, "join").mockReturnValue(localSchemaPath); vi.spyOn(fs, "readFile").mockResolvedValue(JSON.stringify({})); - vi.spyOn(console, "error").mockImplementation(() => {}); - const result = await readLocalSchema(localSchemaPath, collectionName); expect(result).toBeUndefined(); }); @@ -62,7 +58,6 @@ describe("readLocalSchema", () => { test("should handle file read errors gracefully", async () => { vi.spyOn(path, "join").mockReturnValue(localSchemaPath); vi.spyOn(fs, "readFile").mockRejectedValue(new Error("File read error")); - vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readLocalSchema(localSchemaPath, collectionName); expect(result).toBeUndefined(); diff --git a/vitest.config-e2e.ts b/vitest.config-e2e.ts new file mode 100644 index 0000000..b01678d --- /dev/null +++ b/vitest.config-e2e.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + include: ["test/**/*.spec-e2e.ts"], + silent: true + } +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2512f8a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + include: ["test/**/*.spec.ts"], + silent: true + } +});