test(e2e): Playwright + Vitest coverage harness and test instrumentation
Add an end-to-end and unit test suite for the SvelteKit frontend:
- Playwright e2e specs (tests/e2e/spa) with a throwaway container stack,
codegen scenarios, and an Istanbul-based coverage report pipeline.
- Vitest unit tests across API endpoints, components, stores and composables.
- `data-testid` hooks on interactive elements (AppShell, FileViewer,
ShareDialog, search, photos, files breadcrumbs, login/Nextcloud flows,
public share pages) so the e2e suite can target them deterministically.
- Serve the SPA app-shell CSP from a <meta> policy (svelte.config.js) plus a
middleware that skips the CSP header on HTML; move the Nextcloud Login Flow
v2 grant page to the SvelteKit /nextcloud/login route.
- `just front-codegen` recipe and start-server-spa.sh harness.
Make the test environment robust and consistent:
- Install a deterministic in-memory localStorage/sessionStorage in the Vitest
setup so storage behaves identically across Node versions (Node 26 ships a
native Web Storage global that otherwise shadows jsdom's).
- Pin devenv to Node 26 + PostgreSQL 18 and pin every CI job to Node 26.3.0
so the dev shell and CI run the same toolchain versions.
Repair the API/WebDAV (hurl) suite, which had drifted from the backend:
- Migrate the removed `/api/folders/{id}/listing` endpoint to `/resources`
(cursor-paginated `{items:[{resource_type,resource}]}` shape) across the
batch-copy, grants, nested-group, and WebDAV NC tests + the dav_helpers
wipe routine.
- Stop photos_etag from uploading the dedup-tracked fixture so the dedup
blob-lifecycle test can own its content-addressed blob exclusively.
- dedup_create now asserts the idempotent same-content re-upload (201 +
existing file id) instead of the stale 409 expectation.
Generated coverage reports, nyc output and the e2e server runtime data dir
are gitignored rather than committed.
This commit is contained in:
+63
-4
@@ -1,3 +1,13 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# Selects which builder stage assembles the runtime image. Defaults reproduce
|
||||
# the CI/release path exactly (the `builder` stage; binaries under
|
||||
# target/release). The e2e image build overrides these to
|
||||
# BUILDER=builder-cache / BIN_DIR=/app/bin to use the BuildKit cache-mount
|
||||
# builder. Declared in the global scope because FROM (unlike COPY --from) can
|
||||
# expand a build arg in a stage reference.
|
||||
ARG BUILDER=builder
|
||||
ARG BIN_DIR=/app/target/release
|
||||
|
||||
# ─── Stage 1: Shared build base (avoids duplicate apk install) ────────────────
|
||||
FROM rust:1.96-alpine3.24 AS base
|
||||
# sqlx's postgres driver speaks the wire protocol in pure Rust (no pq-sys in
|
||||
@@ -12,8 +22,15 @@ RUN apk --no-cache upgrade && \
|
||||
FROM node:26.3.1-alpine3.24 AS frontend
|
||||
WORKDIR /frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
# Cache mount for npm's package store: when the lockfile changes (busting the
|
||||
# layer) npm ci still reuses already-downloaded tarballs instead of refetching
|
||||
# them. Persists in the local BuildKit cache; ignored harmlessly when absent.
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci
|
||||
COPY frontend/ ./
|
||||
# VITE_E2E=1 keeps the test-only `data-testid` attributes in the build (set by
|
||||
# the e2e image build); unset for release images, which strip them entirely.
|
||||
ARG VITE_E2E
|
||||
ENV VITE_E2E=${VITE_E2E}
|
||||
RUN npm run build
|
||||
|
||||
# ─── Stage 2: Cache dependencies ─────────────────────────────────────────────
|
||||
@@ -54,6 +71,48 @@ RUN DATABASE_URL="${DATABASE_URL}" cargo build --release --bin oxicloud --bin ge
|
||||
# below (build.rs has no asset pipeline — it only injects git metadata).
|
||||
COPY --from=frontend /static-dist ./static-dist
|
||||
|
||||
# ─── Stage 3b: Cache-mount builder (local e2e fast incremental rebuilds) ──────
|
||||
# Built ONLY when BUILDER=builder-cache is passed (the Testcontainers e2e build,
|
||||
# which calls .withBuildkit()). BuildKit cache mounts persist the cargo registry
|
||||
# and target/ in the local BuildKit cache across runs, so a one-line src change
|
||||
# recompiles just the changed crate instead of the whole dependency graph. CI
|
||||
# never sets this arg, so this stage is absent from CI's build graph and CI
|
||||
# behaviour/caching is unchanged.
|
||||
#
|
||||
# NOTE: target/ is a cache mount, so it is NOT part of the image layer once the
|
||||
# RUN finishes — the two shipped binaries MUST be cp'd out within the same RUN.
|
||||
# TARGETARCH scopes the target/ mount per-arch so it is never shared across
|
||||
# architectures (object files are arch-specific).
|
||||
FROM base AS builder-cache
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock build.rs ./
|
||||
COPY src src
|
||||
COPY static static
|
||||
COPY migrations migrations
|
||||
COPY templates templates
|
||||
COPY --from=frontend /static-dist ./static-dist
|
||||
ARG DATABASE_URL="postgres://postgres:postgres@localhost/oxicloud"
|
||||
ARG TARGETARCH
|
||||
RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry,sharing=shared \
|
||||
--mount=type=cache,id=cargo-git,target=/usr/local/cargo/git,sharing=shared \
|
||||
--mount=type=cache,id=oxicloud-target-${TARGETARCH},target=/app/target,sharing=locked \
|
||||
DATABASE_URL="${DATABASE_URL}" cargo build --release && \
|
||||
mkdir -p /app/bin && \
|
||||
cp target/release/oxicloud /app/bin/oxicloud && \
|
||||
cp target/release/migrate-nfc-filenames /app/bin/migrate-nfc-filenames
|
||||
|
||||
# ─── Stage 3c: Select the builder & normalise the binary path ─────────────────
|
||||
# FROM expands the global ${BUILDER} arg to alias the chosen builder stage
|
||||
# (`builder` for CI/release, `builder-cache` for the e2e image). It then copies
|
||||
# the two shipped binaries from the builder-specific ${BIN_DIR} into a single
|
||||
# stable path (/app/release) so the runtime stage's COPYs are independent of
|
||||
# which builder ran. `static-dist` already lives at /app/static-dist in both
|
||||
# builders, so it needs no normalisation.
|
||||
FROM ${BUILDER} AS app
|
||||
ARG BIN_DIR
|
||||
RUN mkdir -p /app/release && \
|
||||
cp "${BIN_DIR}/oxicloud" "${BIN_DIR}/migrate-nfc-filenames" /app/release/
|
||||
|
||||
# ─── Stage 4: Minimal runtime image ──────────────────────────────────────────
|
||||
FROM alpine:3.24.0
|
||||
|
||||
@@ -76,19 +135,19 @@ RUN apk --no-cache upgrade && \
|
||||
adduser -u 1001 -S oxicloud -G oxicloud
|
||||
|
||||
# Copy the compiled binary and entrypoint (--chmod avoids extra RUN chmod layers)
|
||||
COPY --from=builder --chmod=755 /app/target/release/oxicloud /usr/local/bin/
|
||||
COPY --from=app --chmod=755 /app/release/oxicloud /usr/local/bin/
|
||||
# Ship the NFC filename migration binary alongside the server so
|
||||
# operators can run it inside the container without a separate Rust
|
||||
# toolchain — `docker exec <container> migrate-nfc-filenames --dry-run`
|
||||
# to preview, drop `--dry-run` to execute. One-shot tool, safe to
|
||||
# ship; it only mutates `storage.files` rows whose name ≠ NFC(name).
|
||||
COPY --from=builder --chmod=755 /app/target/release/migrate-nfc-filenames /usr/local/bin/
|
||||
COPY --from=app --chmod=755 /app/release/migrate-nfc-filenames /usr/local/bin/
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN sed -i 's/\r//' /usr/local/bin/entrypoint.sh && \
|
||||
chmod 755 /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Copy the built SPA (produced by the Vite frontend stage)
|
||||
COPY --from=builder --chown=oxicloud:oxicloud /app/static-dist /app/static
|
||||
COPY --from=app --chown=oxicloud:oxicloud /app/static-dist /app/static
|
||||
# Create storage directory with proper permissions
|
||||
RUN mkdir -p /app/storage && chown -R oxicloud:oxicloud /app/storage
|
||||
|
||||
|
||||
Reference in New Issue
Block a user