@nexttylabs/echo 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +12 -6
- package/app/(dashboard)/admin/feedback/new/page.tsx +19 -17
- package/app/(dashboard)/admin/layout.tsx +16 -6
- package/app/(dashboard)/layout.tsx +4 -2
- package/app/(dashboard)/settings/api-keys/page.tsx +13 -3
- package/app/(dashboard)/settings/layout.tsx +25 -2
- package/app/(dashboard)/settings/organization/page.tsx +8 -9
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
- package/app/api/admin/backup/route.ts +22 -4
- package/app/api/auth/register/handler.ts +1 -2
- package/app/api/feedback/[id]/comments/[commentId]/route.ts +13 -4
- package/app/api/feedback/[id]/reclassify/route.ts +4 -4
- package/app/api/organizations/handler.ts +2 -4
- package/components/settings/settings-sidebar.tsx +4 -4
- package/hooks/use-organization.tsx +116 -0
- package/hooks/use-permissions.ts +24 -11
- package/lib/auth/config.ts +0 -7
- package/lib/auth/organization.ts +20 -0
- package/lib/auth/permissions.ts +10 -0
- package/lib/db/migrations/0000_needy_leech.sql +335 -0
- package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
- package/lib/db/migrations/meta/_journal.json +2 -135
- package/lib/db/schema/auth.ts +0 -1
- package/lib/db/schema/index.ts +0 -1
- package/lib/portal/public-context.tsx +5 -0
- package/package.json +20 -1
- package/.changeset/README.md +0 -21
- package/.changeset/config.json +0 -11
- package/.changeset/cozy-ghosts-care.md +0 -5
- package/.changeset/sharp-lines-stand.md +0 -5
- package/.changeset/sour-doodles-eat.md +0 -5
- package/.changeset/tender-moose-shop.md +0 -5
- package/.github/pull_request_template.md +0 -13
- package/.github/workflows/ci.yml +0 -41
- package/.github/workflows/publish.yml +0 -44
- package/.github/workflows/release.yml +0 -73
- package/AGENTS.md +0 -92
- package/Dockerfile +0 -57
- package/Makefile +0 -77
- package/bun.lock +0 -2503
- package/components/portal/project-switcher.tsx +0 -20
- package/docker-compose.dev.yml +0 -26
- package/docker-compose.yml +0 -98
- package/docs/architecture.md +0 -259
- package/docs/component-inventory.md +0 -261
- package/docs/database-migrations.md +0 -76
- package/docs/development-guide.md +0 -209
- package/docs/e2e-user-flows.csv +0 -31
- package/docs/er-diagram-feedback.mmd +0 -138
- package/docs/er-diagram.mmd +0 -281
- package/docs/i18n-check-report.md +0 -296
- package/docs/index.md +0 -214
- package/docs/logic-chain.md +0 -94
- package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
- package/docs/plans/2026-01-02-user-login-design.md +0 -37
- package/docs/plans/2026-01-02-user-login.md +0 -437
- package/docs/plans/2026-01-02-user-registration-design.md +0 -47
- package/docs/plans/2026-01-02-user-registration.md +0 -628
- package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
- package/docs/plans/2026-01-03-roles-permissions.md +0 -266
- package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
- package/docs/plans/2026-01-05-member-removal.md +0 -186
- package/docs/plans/2026-01-05-organization-creation.md +0 -374
- package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
- package/docs/plans/2026-01-05-role-configuration.md +0 -441
- package/docs/plans/2026-01-06-file-upload-support.md +0 -804
- package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
- package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
- package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
- package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
- package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
- package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
- package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
- package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
- package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
- package/docs/plans/2026-01-09-settings-center-design.md +0 -114
- package/docs/plans/2026-01-09-settings-center.md +0 -948
- package/docs/plans/2026-01-10-organization-only-design.md +0 -66
- package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
- package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
- package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
- package/docs/plans/2026-01-18-changesets-design.md +0 -40
- package/docs/product_changes.md +0 -37
- package/docs/project-overview.md +0 -159
- package/docs/project-scan-report.json +0 -104
- package/docs/route-role-visibility.md +0 -51
- package/docs/source-tree-analysis.md +0 -150
- package/docs/testing/delete-project-manual-tests.md +0 -18
- package/docs/user-story-tracking.md +0 -191
- package/eslint.config.mjs +0 -19
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
- package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
- package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
- package/lib/db/migrations/0003_add_org_description.sql +0 -1
- package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
- package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
- package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
- package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
- package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
- package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
- package/lib/db/migrations/0010_kind_junta.sql +0 -8
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
- package/lib/db/migrations/0012_giant_power_man.sql +0 -24
- package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
- package/lib/db/migrations/0014_blue_alice.sql +0 -18
- package/lib/db/migrations/0015_webhook_tables.sql +0 -41
- package/lib/db/migrations/0016_github_integration.sql +0 -30
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
- package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
- package/lib/db/migrations/0018_same_spitfire.sql +0 -1
- package/lib/db/migrations/0019_jittery_loners.sql +0 -16
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
- package/lib/db/migrations/meta/0001_snapshot.json +0 -553
- package/lib/db/migrations/meta/0002_snapshot.json +0 -560
- package/lib/db/migrations/meta/0003_snapshot.json +0 -650
- package/lib/db/migrations/meta/0004_snapshot.json +0 -852
- package/lib/db/migrations/meta/0005_snapshot.json +0 -900
- package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
- package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
- package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
- package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
- package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
- package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
- package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
- package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
- package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
- package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
- package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
- package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
- package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
- package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
- package/lib/db/schema/projects.ts +0 -145
- package/lib/db/schema/user-profiles.ts +0 -31
- package/lib/validations/projects.ts +0 -49
- package/next-env.d.ts +0 -6
- package/playwright.config.ts +0 -44
- package/proxy.test.ts +0 -131
- package/proxy.ts +0 -116
- package/scripts/backup-db.sh +0 -57
- package/scripts/backup-db.ts +0 -24
- package/scripts/generate-openapi.ts +0 -22
- package/scripts/migration-helper.ts +0 -39
- package/scripts/pre-deploy.ts +0 -75
- package/scripts/restore-db.sh +0 -60
- package/scripts/rollback.ts +0 -72
- package/scripts/seed-tags.ts +0 -48
- package/tests/api/feedback-bulk.test.ts +0 -47
- package/tests/api/feedback-by-id.test.ts +0 -67
- package/tests/api/feedback-comments-route-import.test.ts +0 -26
- package/tests/api/feedback-create.test.ts +0 -71
- package/tests/api/feedback-delete.test.ts +0 -160
- package/tests/api/feedback-filter.test.ts +0 -250
- package/tests/api/feedback-list.test.ts +0 -234
- package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
- package/tests/api/feedback-similar.test.ts +0 -46
- package/tests/api/feedback-sort.test.ts +0 -261
- package/tests/api/feedback-status-enum.test.ts +0 -49
- package/tests/api/feedback-status-filter.test.ts +0 -117
- package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
- package/tests/api/feedback.test.ts +0 -175
- package/tests/api/identify-jwt.test.ts +0 -25
- package/tests/api/invitation-accept.test.ts +0 -213
- package/tests/api/organization-invitations.test.ts +0 -186
- package/tests/api/organization-members-list.test.ts +0 -79
- package/tests/api/organization-members.test.ts +0 -340
- package/tests/api/organizations.test.ts +0 -149
- package/tests/api/register.test.ts +0 -112
- package/tests/api/upload.test.ts +0 -103
- package/tests/api/vote.test.ts +0 -82
- package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
- package/tests/app/admin-feedback-list-page.test.tsx +0 -25
- package/tests/app/admin-feedback-new-page.test.tsx +0 -25
- package/tests/app/health-route-helpers.test.ts +0 -27
- package/tests/app/login-page.test.ts +0 -26
- package/tests/app/portal-page.test.ts +0 -29
- package/tests/app/project-portal-overview.test.tsx +0 -25
- package/tests/app/widget-page-import.test.ts +0 -25
- package/tests/components/create-post-dialog-defaults.test.ts +0 -43
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
- package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
- package/tests/components/feedback/feedback-detail.test.tsx +0 -25
- package/tests/components/feedback/feedback-stats.test.tsx +0 -49
- package/tests/components/feedback-bulk-actions.test.tsx +0 -39
- package/tests/components/feedback-i18n-keys.test.ts +0 -70
- package/tests/components/feedback-list-controls-compile.test.ts +0 -25
- package/tests/components/feedback-list-controls.test.tsx +0 -204
- package/tests/components/feedback-list-item.test.tsx +0 -67
- package/tests/components/landing/hero.test.tsx +0 -46
- package/tests/components/layout/language-switcher.test.tsx +0 -25
- package/tests/components/layout/sidebar.test.tsx +0 -157
- package/tests/components/login-form.test.ts +0 -25
- package/tests/components/organization-form.test.ts +0 -32
- package/tests/components/organization-switcher.test.ts +0 -25
- package/tests/components/pagination.test.tsx +0 -43
- package/tests/components/portal-overview.test.tsx +0 -25
- package/tests/components/profile-form.test.tsx +0 -139
- package/tests/components/role-selector.test.ts +0 -31
- package/tests/components/status-chart.test.tsx +0 -90
- package/tests/e2e/auth.e2e.ts +0 -323
- package/tests/e2e/feedback-actions.e2e.ts +0 -471
- package/tests/e2e/feedback-attachment.e2e.ts +0 -168
- package/tests/e2e/feedback-customer.e2e.ts +0 -226
- package/tests/e2e/feedback-management.e2e.ts +0 -565
- package/tests/e2e/feedback-submit.e2e.ts +0 -133
- package/tests/e2e/feedback-view.e2e.ts +0 -297
- package/tests/e2e/fixtures/test-data.ts +0 -235
- package/tests/e2e/health-check.e2e.ts +0 -230
- package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
- package/tests/e2e/helpers/test-utils.ts +0 -298
- package/tests/e2e/integration-placeholders.e2e.ts +0 -199
- package/tests/e2e/organization.e2e.ts +0 -292
- package/tests/e2e/permissions.e2e.ts +0 -424
- package/tests/e2e/project-widget.e2e.ts +0 -63
- package/tests/feedback/filters.test.ts +0 -29
- package/tests/hooks/use-permissions.test.ts +0 -52
- package/tests/lib/ai/classifier.test.ts +0 -104
- package/tests/lib/ai/duplicate-detector.test.ts +0 -234
- package/tests/lib/attachments-schema.test.ts +0 -30
- package/tests/lib/auth/session.test.ts +0 -49
- package/tests/lib/auth-client.test.ts +0 -37
- package/tests/lib/auth-config.test.ts +0 -26
- package/tests/lib/feedback-prefill.test.ts +0 -52
- package/tests/lib/feedback-processor.test.ts +0 -41
- package/tests/lib/feedback-schema.test.ts +0 -33
- package/tests/lib/file-validator.test.ts +0 -48
- package/tests/lib/get-feedback-by-id.test.ts +0 -37
- package/tests/lib/invitations.test.ts +0 -35
- package/tests/lib/login-schema.test.ts +0 -36
- package/tests/lib/org-context.test.ts +0 -95
- package/tests/lib/organization-access.test.ts +0 -44
- package/tests/lib/organization-member-role-schema.test.ts +0 -41
- package/tests/lib/permissions.test.ts +0 -88
- package/tests/lib/portal-analytics.test.ts +0 -25
- package/tests/lib/portal-contributors.test.ts +0 -25
- package/tests/lib/portal-copy.test.ts +0 -27
- package/tests/lib/portal-i18n.test.ts +0 -30
- package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
- package/tests/lib/portal-modules.test.ts +0 -25
- package/tests/lib/portal-seo.test.ts +0 -25
- package/tests/lib/portal-sharing.test.ts +0 -25
- package/tests/lib/portal-sorting.test.ts +0 -25
- package/tests/lib/portal-theme.test.ts +0 -25
- package/tests/lib/rate-limit.test.ts +0 -142
- package/tests/lib/resolve-locale.test.ts +0 -34
- package/tests/lib/services/backup.test.ts +0 -145
- package/tests/lib/user-organizations.test.ts +0 -42
- package/tests/lib/user-role-schema.test.ts +0 -33
- package/tests/lib/user-schema.test.ts +0 -25
- package/tests/setup.ts +0 -74
- package/vercel.json +0 -4
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { test, expect } from "@playwright/test";
|
|
19
|
-
import { isHealthStatusOk } from "./helpers/test-utils";
|
|
20
|
-
|
|
21
|
-
test.describe("E2E-UF-029: Health check endpoint", () => {
|
|
22
|
-
test("returns 200 status for health endpoint", async ({ request }) => {
|
|
23
|
-
const response = await request.get("/health");
|
|
24
|
-
|
|
25
|
-
expect(response.status()).toBe(200);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("returns health status information", async ({ request }) => {
|
|
29
|
-
const response = await request.get("/health");
|
|
30
|
-
|
|
31
|
-
expect(response.status()).toBe(200);
|
|
32
|
-
|
|
33
|
-
const health = await response.json();
|
|
34
|
-
|
|
35
|
-
// Should contain status field
|
|
36
|
-
expect(health).toHaveProperty("status");
|
|
37
|
-
expect(["ok", "healthy"]).toContain(health.status);
|
|
38
|
-
|
|
39
|
-
// Might contain timestamp
|
|
40
|
-
if (health.timestamp) {
|
|
41
|
-
expect(["string", "number"]).toContain(typeof health.timestamp);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Might contain uptime
|
|
45
|
-
if (health.uptime) {
|
|
46
|
-
expect(typeof health.uptime).toBe("number");
|
|
47
|
-
expect(health.uptime).toBeGreaterThan(0);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("includes database connectivity status", async ({ request }) => {
|
|
52
|
-
const response = await request.get("/health");
|
|
53
|
-
|
|
54
|
-
expect(response.status()).toBe(200);
|
|
55
|
-
|
|
56
|
-
const health = await response.json();
|
|
57
|
-
|
|
58
|
-
// Should check database connection
|
|
59
|
-
if (health.checks) {
|
|
60
|
-
expect(health.checks).toHaveProperty("database");
|
|
61
|
-
expect(["ok", "connected"]).toContain(health.checks.database.status);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("includes service dependencies", async ({ request }) => {
|
|
66
|
-
const response = await request.get("/health");
|
|
67
|
-
|
|
68
|
-
expect(response.status()).toBe(200);
|
|
69
|
-
|
|
70
|
-
const health = await response.json();
|
|
71
|
-
|
|
72
|
-
// Might include other service checks
|
|
73
|
-
if (health.checks) {
|
|
74
|
-
// Check for Redis if used
|
|
75
|
-
if (health.checks.redis) {
|
|
76
|
-
expect(isHealthStatusOk(health.checks.redis.status)).toBe(true);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check for email service if configured
|
|
80
|
-
if (health.checks.email) {
|
|
81
|
-
expect(["ok", "configured", "warning"]).toContain(health.checks.email.status);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("responds quickly", async ({ request }) => {
|
|
87
|
-
const startTime = Date.now();
|
|
88
|
-
|
|
89
|
-
const response = await request.get("/health");
|
|
90
|
-
|
|
91
|
-
const endTime = Date.now();
|
|
92
|
-
const responseTime = endTime - startTime;
|
|
93
|
-
|
|
94
|
-
expect(response.status()).toBe(200);
|
|
95
|
-
expect(responseTime).toBeLessThan(1000); // Should respond within 1 second
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("handles concurrent requests", async ({ request }) => {
|
|
99
|
-
// Make multiple concurrent requests
|
|
100
|
-
const promises = Array(10).fill(null).map(() => request.get("/health"));
|
|
101
|
-
|
|
102
|
-
const responses = await Promise.all(promises);
|
|
103
|
-
|
|
104
|
-
// All should succeed
|
|
105
|
-
responses.forEach(response => {
|
|
106
|
-
expect(response.status()).toBe(200);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("includes version information", async ({ request }) => {
|
|
111
|
-
const response = await request.get("/health");
|
|
112
|
-
|
|
113
|
-
expect(response.status()).toBe(200);
|
|
114
|
-
|
|
115
|
-
const health = await response.json();
|
|
116
|
-
|
|
117
|
-
// Might include version
|
|
118
|
-
if (health.version) {
|
|
119
|
-
expect(typeof health.version).toBe("string");
|
|
120
|
-
expect(health.version.length).toBeGreaterThan(0);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("returns proper headers", async ({ request }) => {
|
|
125
|
-
const response = await request.get("/health");
|
|
126
|
-
|
|
127
|
-
expect(response.status()).toBe(200);
|
|
128
|
-
|
|
129
|
-
// Should have content-type
|
|
130
|
-
const contentType = response.headers()["content-type"];
|
|
131
|
-
expect(contentType).toMatch(/application\/json/);
|
|
132
|
-
|
|
133
|
-
// Should have cache control headers for health endpoint
|
|
134
|
-
const cacheControl = response.headers()["cache-control"];
|
|
135
|
-
if (cacheControl) {
|
|
136
|
-
expect(cacheControl).toContain("no-cache");
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("health endpoint works during high load", async ({ request }) => {
|
|
141
|
-
// Simulate high load with many requests
|
|
142
|
-
const promises = Array(100).fill(null).map(async () => {
|
|
143
|
-
const response = await request.get("/health");
|
|
144
|
-
expect(response.status()).toBe(200);
|
|
145
|
-
return response;
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const responses = await Promise.all(promises);
|
|
149
|
-
|
|
150
|
-
// All should succeed
|
|
151
|
-
expect(responses).toHaveLength(100);
|
|
152
|
-
responses.forEach(response => {
|
|
153
|
-
expect(response.status()).toBe(200);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("graceful degradation when services are down", async ({ request }) => {
|
|
158
|
-
// This test would require mocking service failures
|
|
159
|
-
// For now, just verify the endpoint responds
|
|
160
|
-
|
|
161
|
-
const response = await request.get("/health");
|
|
162
|
-
|
|
163
|
-
// Should always respond, even if some services are down
|
|
164
|
-
expect(response.status()).toBe(200);
|
|
165
|
-
|
|
166
|
-
const health = await response.json();
|
|
167
|
-
|
|
168
|
-
// If services are down, status might be degraded but endpoint should still work
|
|
169
|
-
if (health.status !== "ok") {
|
|
170
|
-
expect(["degraded", "warning"]).toContain(health.status);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("includes performance metrics", async ({ request }) => {
|
|
175
|
-
const response = await request.get("/health");
|
|
176
|
-
|
|
177
|
-
expect(response.status()).toBe(200);
|
|
178
|
-
|
|
179
|
-
const health = await response.json();
|
|
180
|
-
|
|
181
|
-
// Might include performance metrics
|
|
182
|
-
if (health.metrics) {
|
|
183
|
-
// Memory usage
|
|
184
|
-
if (health.metrics.memory) {
|
|
185
|
-
expect(typeof health.metrics.memory.used).toBe("number");
|
|
186
|
-
expect(typeof health.metrics.memory.total).toBe("number");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// CPU usage
|
|
190
|
-
if (health.metrics.cpu) {
|
|
191
|
-
expect(typeof health.metrics.cpu.usage).toBe("number");
|
|
192
|
-
expect(health.metrics.cpu.usage).toBeGreaterThanOrEqual(0);
|
|
193
|
-
expect(health.metrics.cpu.usage).toBeLessThanOrEqual(100);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("accessible without authentication", async ({ request }) => {
|
|
199
|
-
// Health endpoint should be publicly accessible
|
|
200
|
-
const response = await request.get("/health", {
|
|
201
|
-
headers: {
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(response.status()).toBe(200);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("supports different health check levels", async ({ request }) => {
|
|
209
|
-
// Basic health check
|
|
210
|
-
const basicResponse = await request.get("/health");
|
|
211
|
-
expect(basicResponse.status()).toBe(200);
|
|
212
|
-
|
|
213
|
-
// Detailed health check (if supported)
|
|
214
|
-
const detailedResponse = await request.get("/health?detailed=true");
|
|
215
|
-
expect(detailedResponse.status()).toBe(200);
|
|
216
|
-
|
|
217
|
-
// Detailed check might include more information
|
|
218
|
-
const basicHealth = await basicResponse.json();
|
|
219
|
-
const detailedHealth = await detailedResponse.json();
|
|
220
|
-
|
|
221
|
-
// Both should have at least status
|
|
222
|
-
expect(basicHealth).toHaveProperty("status");
|
|
223
|
-
expect(detailedHealth).toHaveProperty("status");
|
|
224
|
-
|
|
225
|
-
// Detailed might have more fields
|
|
226
|
-
if (detailedHealth.checks && !basicHealth.checks) {
|
|
227
|
-
expect(Object.keys(detailedHealth).length).toBeGreaterThan(Object.keys(basicHealth).length);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, expect, it } from "bun:test";
|
|
19
|
-
import { isHealthStatusOk, resolveOrganizationSlug } from "./test-utils";
|
|
20
|
-
|
|
21
|
-
describe("resolveOrganizationSlug", () => {
|
|
22
|
-
it("returns slug when present", () => {
|
|
23
|
-
const slug = resolveOrganizationSlug({
|
|
24
|
-
data: { organization: { slug: "acme" } },
|
|
25
|
-
});
|
|
26
|
-
expect(slug).toBe("acme");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("throws when slug is missing", () => {
|
|
30
|
-
expect(() => resolveOrganizationSlug({ data: {} })).toThrow();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("isHealthStatusOk", () => {
|
|
35
|
-
it("returns true for ok or connected", () => {
|
|
36
|
-
expect(isHealthStatusOk("ok")).toBe(true);
|
|
37
|
-
expect(isHealthStatusOk("connected")).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns false for other values", () => {
|
|
41
|
-
expect(isHealthStatusOk("warning")).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { test as base, Page, APIRequestContext, expect } from "@playwright/test";
|
|
19
|
-
|
|
20
|
-
// Test data generators
|
|
21
|
-
export function uniqueEmail() {
|
|
22
|
-
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
23
|
-
return `e2e+${suffix}@example.com`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function uniqueTitle() {
|
|
27
|
-
return `E2E feedback ${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function uniqueName() {
|
|
31
|
-
return `E2E User ${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function uniqueOrganizationName() {
|
|
35
|
-
return `E2E Org ${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function uniqueProjectName() {
|
|
39
|
-
return `E2E Project ${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function resolveOrganizationSlug(json: unknown): string {
|
|
43
|
-
const slug =
|
|
44
|
-
typeof json === "object" && json
|
|
45
|
-
? (json as { data?: { organization?: { slug?: string } } }).data
|
|
46
|
-
?.organization?.slug
|
|
47
|
-
: undefined;
|
|
48
|
-
if (!slug) {
|
|
49
|
-
throw new Error("Failed to get organization slug");
|
|
50
|
-
}
|
|
51
|
-
return slug;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isHealthStatusOk(status: string | undefined): boolean {
|
|
55
|
-
return status === "ok" || status === "connected";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Test fixtures
|
|
59
|
-
export interface TestFixtures {
|
|
60
|
-
authenticatedPage: Page;
|
|
61
|
-
organizationSlug: string;
|
|
62
|
-
adminPage: Page;
|
|
63
|
-
testUser: {
|
|
64
|
-
email: string;
|
|
65
|
-
name: string;
|
|
66
|
-
password: string;
|
|
67
|
-
};
|
|
68
|
-
adminUser: {
|
|
69
|
-
email: string;
|
|
70
|
-
name: string;
|
|
71
|
-
password: string;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const test = base.extend<TestFixtures>({
|
|
76
|
-
testUser: async ({}, use) => {
|
|
77
|
-
const user = {
|
|
78
|
-
email: uniqueEmail(),
|
|
79
|
-
name: uniqueName(),
|
|
80
|
-
password: "StrongPass123!",
|
|
81
|
-
};
|
|
82
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
83
|
-
await use(user);
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
adminUser: async ({}, use) => {
|
|
87
|
-
const user = {
|
|
88
|
-
email: `admin+${Date.now()}@example.com`,
|
|
89
|
-
name: `Admin User ${Date.now()}`,
|
|
90
|
-
password: "AdminPass123!",
|
|
91
|
-
};
|
|
92
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
93
|
-
await use(user);
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
organizationSlug: async ({ testUser, request }, use) => {
|
|
97
|
-
// Register user and create organization
|
|
98
|
-
const register = await request.post("/api/auth/register", {
|
|
99
|
-
data: testUser,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (!register.ok()) {
|
|
103
|
-
throw new Error("Failed to register test user");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const json = await register.json();
|
|
107
|
-
const slug = resolveOrganizationSlug(json);
|
|
108
|
-
|
|
109
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
110
|
-
await use(slug);
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
authenticatedPage: async ({ page, testUser }, use) => {
|
|
114
|
-
// Login user
|
|
115
|
-
await page.goto("/login");
|
|
116
|
-
await page.locator('input[name="email"], #email').fill(testUser.email);
|
|
117
|
-
await page.locator('input[name="password"], #password').fill(testUser.password);
|
|
118
|
-
await page
|
|
119
|
-
.getByRole("button", { name: /sign in|log in|login|登录|ログイン/i })
|
|
120
|
-
.click();
|
|
121
|
-
|
|
122
|
-
// Wait for navigation
|
|
123
|
-
await page.waitForURL(/\/dashboard/);
|
|
124
|
-
|
|
125
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
126
|
-
await use(page);
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
adminPage: async ({ page, adminUser, request }, use) => {
|
|
130
|
-
// Register admin user
|
|
131
|
-
const register = await request.post("/api/auth/register", {
|
|
132
|
-
data: adminUser,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (!register.ok()) {
|
|
136
|
-
throw new Error("Failed to register admin user");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Login as admin
|
|
140
|
-
await page.goto("/login");
|
|
141
|
-
await page.locator('input[name="email"], #email').fill(adminUser.email);
|
|
142
|
-
await page.locator('input[name="password"], #password').fill(adminUser.password);
|
|
143
|
-
await page
|
|
144
|
-
.getByRole("button", { name: /sign in|log in|login|登录|ログイン/i })
|
|
145
|
-
.click();
|
|
146
|
-
|
|
147
|
-
// Wait for navigation
|
|
148
|
-
await page.waitForURL(/\/dashboard/);
|
|
149
|
-
|
|
150
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
151
|
-
await use(page);
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Common test helpers
|
|
156
|
-
export class TestHelpers {
|
|
157
|
-
private organizationId?: string;
|
|
158
|
-
private organizationSlug?: string;
|
|
159
|
-
|
|
160
|
-
constructor(private page: Page, private request: APIRequestContext) {}
|
|
161
|
-
|
|
162
|
-
async createFeedback(data: {
|
|
163
|
-
title: string;
|
|
164
|
-
description: string;
|
|
165
|
-
type?: string;
|
|
166
|
-
priority?: string;
|
|
167
|
-
organizationSlug: string;
|
|
168
|
-
}) {
|
|
169
|
-
await this.page.goto(`/${data.organizationSlug}`);
|
|
170
|
-
await this.page.getByRole("button", { name: "Submit Feedback" }).click();
|
|
171
|
-
|
|
172
|
-
await this.page.getByLabel("Title").fill(data.title);
|
|
173
|
-
await this.page.getByLabel("Description").fill(data.description);
|
|
174
|
-
|
|
175
|
-
if (data.type) {
|
|
176
|
-
const typeSelect = this.page.getByLabel("Type");
|
|
177
|
-
if (await typeSelect.isVisible()) {
|
|
178
|
-
await typeSelect.click();
|
|
179
|
-
await this.page.getByRole("option", { name: data.type }).click();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (data.priority) {
|
|
184
|
-
const prioritySelect = this.page.getByLabel("Priority");
|
|
185
|
-
if (await prioritySelect.isVisible()) {
|
|
186
|
-
await prioritySelect.click();
|
|
187
|
-
await this.page.getByRole("option", { name: data.priority }).click();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
await this.page.getByRole("button", { name: "Create Post" }).click();
|
|
192
|
-
|
|
193
|
-
// Prefer returning the detail URL if available.
|
|
194
|
-
const currentUrl = this.page.url();
|
|
195
|
-
if (/\/feedback\/[^\/]+$/.test(currentUrl)) {
|
|
196
|
-
return currentUrl;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const feedbackLink = this.page
|
|
200
|
-
.getByRole("link", { name: new RegExp(data.title) })
|
|
201
|
-
.first();
|
|
202
|
-
await feedbackLink.waitFor({ state: "visible" });
|
|
203
|
-
const href = await feedbackLink.getAttribute("href");
|
|
204
|
-
if (href) {
|
|
205
|
-
return new URL(href, this.page.url()).toString();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return this.page.url();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async login(email: string, password: string) {
|
|
212
|
-
await this.page.goto("/login");
|
|
213
|
-
await this.page.locator('input[name="email"], #email').fill(email);
|
|
214
|
-
await this.page.locator('input[name="password"], #password').fill(password);
|
|
215
|
-
await this.page
|
|
216
|
-
.getByRole("button", { name: /sign in|log in|login|登录|ログイン/i })
|
|
217
|
-
.click();
|
|
218
|
-
|
|
219
|
-
// Wait for navigation
|
|
220
|
-
await this.page.waitForURL(/\/dashboard/);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async registerAndLogin(name: string, email: string, password: string) {
|
|
224
|
-
// Register
|
|
225
|
-
const register = await this.request.post("/api/auth/register", {
|
|
226
|
-
data: { name, email, password },
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (!register.ok()) {
|
|
230
|
-
throw new Error("Failed to register user");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Login
|
|
234
|
-
await this.login(email, password);
|
|
235
|
-
|
|
236
|
-
const json = await register.json();
|
|
237
|
-
const org = json?.data?.organization;
|
|
238
|
-
this.organizationId = org?.id;
|
|
239
|
-
this.organizationSlug = resolveOrganizationSlug(json);
|
|
240
|
-
return this.organizationSlug;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
getOrganizationId() {
|
|
244
|
-
if (!this.organizationId) {
|
|
245
|
-
throw new Error("Organization ID not available. Call registerAndLogin first.");
|
|
246
|
-
}
|
|
247
|
-
return this.organizationId;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async createOrganization(name: string) {
|
|
251
|
-
await this.page.goto("/admin");
|
|
252
|
-
await this.page.getByRole("button", { name: "Create Organization" }).click();
|
|
253
|
-
|
|
254
|
-
await this.page.getByLabel("Organization Name").fill(name);
|
|
255
|
-
await this.page.getByRole("button", { name: "Create" }).click();
|
|
256
|
-
|
|
257
|
-
// Wait for creation
|
|
258
|
-
await this.page.waitForURL(/\/admin\/organizations\/[^\/]+$/);
|
|
259
|
-
|
|
260
|
-
return this.page.url();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async createProject(name: string, organizationSlug: string) {
|
|
264
|
-
await this.page.goto(`/${organizationSlug}/admin/projects`);
|
|
265
|
-
await this.page.getByRole("button", { name: "Create Project" }).click();
|
|
266
|
-
|
|
267
|
-
await this.page.getByLabel("Project Name").fill(name);
|
|
268
|
-
await this.page.getByRole("button", { name: "Create" }).click();
|
|
269
|
-
|
|
270
|
-
// Wait for creation
|
|
271
|
-
await this.page.waitForURL(/\/admin\/projects\/[^\/]+$/);
|
|
272
|
-
|
|
273
|
-
return this.page.url();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async uploadFile(selector: string, filePath: string) {
|
|
277
|
-
const fileInput = this.page.locator(selector);
|
|
278
|
-
await fileInput.setInputFiles(filePath);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async waitForSuccessMessage() {
|
|
282
|
-
const successIndicator = this.page
|
|
283
|
-
.getByText(/success|created|submitted|updated/i)
|
|
284
|
-
.or(this.page.getByRole("heading").filter({ hasText: /success|created|submitted/i }));
|
|
285
|
-
|
|
286
|
-
await expect(successIndicator).toBeVisible();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async waitForErrorMessage() {
|
|
290
|
-
const errorIndicator = this.page
|
|
291
|
-
.getByText(/error|failed|unable/i)
|
|
292
|
-
.or(this.page.getByRole("alert").filter({ hasText: /error|failed/i }));
|
|
293
|
-
|
|
294
|
-
await expect(errorIndicator).toBeVisible();
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export { expect } from "@playwright/test";
|