diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..87b66d5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +hako +hako.db +dist/ +build/ +devdata/ +.git/ +webapp/node_modules/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2707448 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + format: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - run: make format + - run: git diff --exit-code + + goreleaser-lint: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + steps: + - uses: actions/checkout@v7 + - uses: actions/goreleaser-action@v6.4.0 + with: + args: check + + lint: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - run: make ci-lint + + test: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - run: make test + + e2e: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + services: + dind: + image: docker:27-dind + env: + DOCKER_TLS_CERTDIR: "" + options: --privileged + env: + DOCKER_HOST: tcp://dind:2375 + TESTCONTAINERS_RYUK_DISABLED: true + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: e2e/go.mod + - name: Wait for Docker + run: | + for i in $(seq 1 60); do + docker info >/dev/null 2>&1 && exit 0 + sleep 1 + done + echo "Docker daemon not ready" >&2 + exit 1 + - name: Install Playwright browsers + run: go run github.com/playwright-community/playwright-go/cmd/playwright@v0.5200.1 install chromium + - run: make e2e + + build: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3b22d95 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + tags: ["v*"] + +jobs: + release: + runs-on: docker + container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Install Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + + - name: Setup Docker buildx + run: docker buildx create --use --name multiplatform --driver docker-container + + - name: Login to container registry + run: echo "${{ secrets.FORGEJO_TOKEN }}" | docker login git.nakama.town -u ${{ github.actor }} --password-stdin + + - name: Run GoReleaser + uses: actions/goreleaser-action@v6.4.0 + with: + args: release --clean + env: + GORELEASER_FORCE_TOKEN: gitea + GITEA_TOKEN: ${{ secrets.FORGEJO_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 692a8bd..238bbfc 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -57,6 +57,7 @@ dockers_v2: - "git.nakama.town/fmartingr/hako" tags: - "{{ .Version }}" + - latest dockerfile: Containerfile platforms: - linux/amd64 @@ -65,7 +66,7 @@ dockers_v2: nfpms: - maintainer: Felipe Martin - description: SMTP server to forward messages to shoutrrr endpoints + description: Self-hosted link archival service homepage: https://git.nakama.town/fmartingr/hako license: AGPL-3.0 formats: @@ -119,4 +120,7 @@ changelog: - "^chore\\(deps\\):" release: + gitea: + owner: fmartingr + name: hako prerelease: auto diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml deleted file mode 100644 index 1c04bd4..0000000 --- a/.woodpecker/ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -when: - event: - - push - - pull_request - branch: - - main - -steps: - format: - image: golang:1.24 - commands: - - make format - - git diff --exit-code # Fail if files were changed - - lint-goreleaser: - image: goreleaser/goreleaser:latest - commands: - - goreleaser check - - lint: - image: golang:1.24 - commands: - - make ci-lint - - test: - image: golang:1.24 - commands: - - make test - - e2e: - image: golang:1.24 - environment: - TESTCONTAINERS_RYUK_DISABLED: true - DOCKER_HOST: unix:///var/run/docker.sock - volumes: - - /var/run/docker.sock:/var/run/docker.sock - commands: - # Install system dependencies - - apt-get update && apt-get install -y docker.io curl unzip - - # Install Bun for webapp build - - curl -fsSL https://bun.sh/install | bash - - export PATH="$HOME/.bun/bin:$PATH" - - # Install Playwright browsers - - go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium - - # Run e2e tests - - make e2e - when: - status: [success, failure] diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml deleted file mode 100644 index 96c9447..0000000 --- a/.woodpecker/release.yml +++ /dev/null @@ -1,47 +0,0 @@ -when: - - event: tag - branch: main - -steps: - - name: Release - image: ubuntu:24.04 - environment: - GITEA_TOKEN: - from_secret: GITEA_TOKEN - DOCKER_HOST: unix:///var/run/docker.sock - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - commands: - # Install system dependencies - - apt-get update - - apt-get install -y ca-certificates curl git make wget unzip upx-ucl - - # Install Docker from official repository (for buildx support) - - curl -fsSL https://get.docker.com | sh - - # Install Go 1.23 - - wget -q https://go.dev/dl/go1.23.5.linux-amd64.tar.gz - - tar -C /usr/local -xzf go1.23.5.linux-amd64.tar.gz - - rm go1.23.5.linux-amd64.tar.gz - - export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" - - # Install GoReleaser via go install - - go install github.com/goreleaser/goreleaser/v2@latest - - # Install Bun (using official installer) - - curl -fsSL https://bun.sh/install | bash - - export PATH="$HOME/.bun/bin:$PATH" - - # Setup Docker buildx for multi-platform builds - - docker buildx create --use --name multiplatform --driver docker-container - - # Verify installations - - docker --version - - docker buildx version - - goreleaser --version - - bun --version - - upx --version - - # Authenticate and release - - docker login -u fmartingr -p $GITEA_TOKEN git.nakama.town - - goreleaser release --clean diff --git a/Containerfile b/Containerfile index 8276802..aa0e8e7 100644 --- a/Containerfile +++ b/Containerfile @@ -1,36 +1,20 @@ -# This file is used directly by the goreleaser build -# It is used to build the final container image -# To build the image locally use make build-docker - -# Runtime image -FROM alpine:3.20 +FROM alpine:3.23 LABEL maintainer="Felipe Martin " LABEL org.opencontainers.image.source="https://git.nakama.town/fmartingr/hako" -# Install runtime dependencies (yt-dlp and ffmpeg for video archival from Alpine community) RUN apk add --no-cache \ poppler-utils \ ca-certificates \ tzdata \ - && apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/v3.20/community yt-dlp ffmpeg + && apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/v3.23/community yt-dlp ffmpeg -# Create non-root user -RUN addgroup -g 1000 hako && \ - adduser -D -u 1000 -G hako hako +RUN addgroup -g 1000 hako && adduser -u 1000 -G hako -s /bin/sh -D hako WORKDIR /app +COPY hako /usr/local/bin/hako +RUN chown -R hako:hako /app -# Copy binary from goreleaser build context -# With dockers_v2, goreleaser provides the pre-built binary at $TARGETPLATFORM/hako -# The binary already has the webapp embedded via //go:embed -ARG TARGETPLATFORM -COPY ${TARGETPLATFORM}/hako /usr/local/bin/hako -RUN chmod +x /usr/local/bin/hako && \ - chown -R hako:hako /app - -# Switch to non-root user USER hako - EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/hako"] diff --git a/Makefile b/Makefile index 78ace12..1a62345 100644 --- a/Makefile +++ b/Makefile @@ -1,156 +1,104 @@ PROJECT_NAME := hako - -SOURCE_FILES ?=./... - -TEST_OPTIONS ?= -v -failfast -race -bench=. -benchtime=100000x -cover -coverprofile=coverage.out -TEST_TIMEOUT ?=1m - -GOLANGCI_LINT_VERSION ?= v1.64.5 - -CLEAN_OPTIONS ?=-modcache -testcache - CGO_ENABLED := 0 -BUILDS_PATH := ./dist -FROM_MAKEFILE := y +DIST_PATH := ./dist +TEST_OPTIONS := -v -failfast -race -bench=. -benchtime=100000x -cover -coverprofile=coverage.out +TEST_TIMEOUT := 5m -CONTAINERFILE_NAME := Containerfile -CONTAINER_ALPINE_VERSION := 3.20 -CONTAINER_SOURCE_URL := "https://git.nakama.town/fmartingr/${PROJECT_NAME}" -CONTAINER_MAINTAINER := "Felipe Martin " -CONTAINER_BIN_NAME := ${PROJECT_NAME} +GOLANGCI_LINT_VERSION := v1.64.5 -BUILDX_PLATFORMS := linux/amd64,linux/arm64,linux/arm/v7 +.DEFAULT_GOAL := help -export PROJECT_NAME -export FROM_MAKEFILE - -export CGO_ENABLED - -export SOURCE_FILES -export TEST_OPTIONS -export TEST_TIMEOUT -export BUILDS_PATH - -export CONTAINERFILE_NAME -export CONTAINER_ALPINE_VERSION -export CONTAINER_SOURCE_URL -export CONTAINER_MAINTAINER -export CONTAINER_BIN_NAME - -export BUILDX_PLATFORMS - -.PHONY: all -all: help - -# this is godly -# https://news.ycombinator.com/item?id=11939200 +## help: Display this help message .PHONY: help -help: ### this screen. Keep it first target to be default -ifeq ($(UNAME), Linux) - @grep -P '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ - awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' -else - @# this is not tested, but prepared in advance for you, Mac drivers - @awk -F ':.*###' '$$0 ~ FS {printf "%15s%s\n", $$1 ":", $$2}' \ - $(MAKEFILE_LIST) | grep -v '@awk' | sort -endif +help: + @echo "Available targets:" + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /' | sort +## clean: Clean test cache, build files .PHONY: clean -clean: ### clean test cache, build files - $(info: Make: Clean) - @rm -rf ${BUILDS_PATH} +clean: + @rm -rf $(DIST_PATH) @rm -rf webapp/dist @rm -rf webapp/node_modules @rm -f webapp/bun.lockb - @go clean ${CLEAN_OPTIONS} - @find devdata -mindepth 1 ! -name config.yaml -delete 2>/dev/null || true - @-docker buildx rm ${PROJECT_NAME}_builder + @go clean -modcache -testcache +## clean-devdata: Clear devdata database and files (keeps config.yaml) .PHONY: clean-devdata -clean-devdata: ### Clear devdata database and files (keeps config.yaml) - $(info: Make: Cleaning devdata) +clean-devdata: @rm -rf devdata/data @rm -f devdata/hako.db devdata/hako.db-shm devdata/hako.db-wal +## build: Build project using goreleaser (snapshot) .PHONY: build -build: build-webapp ### builds the project for the setup os/arch combinations - $(info: Make: Build) - @goreleaser --clean --snapshot +build: build-webapp + goreleaser build --snapshot --clean +## build-webapp: Build the webapp .PHONY: build-webapp -build-webapp: ### Build the webapp - $(info: Make: Building webapp) +build-webapp: @which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1) @cd webapp && bun install && bun --bun node_modules/.bin/vite build +## build-docker: Build Docker image locally via goreleaser +.PHONY: build-docker +build-docker: build-webapp + goreleaser release --snapshot --clean + +## run: Build frontend and run the server +.PHONY: run +run: build-webapp run-server + +## run-server: Run the API server only (without building webapp) .PHONY: run-server -run-server: ### Runs the API server only (without building webapp) - $(info: Make: Running server) - CGO_ENABLED=${CGO_ENABLED} go run ./cmd/${PROJECT_NAME}/*.go server --config devdata/config.yaml - -.PHONY: run-quick -run-quick: build-webapp run-server ### Builds the webapp and runs the server +run-server: + CGO_ENABLED=$(CGO_ENABLED) go run ./cmd/$(PROJECT_NAME)/*.go server --config devdata/config.yaml +## run-webapp: Run the webapp in dev mode with hot-reload .PHONY: run-webapp -run-webapp: ### Runs the webapp in dev mode with hot-reload - $(info: Make: Running webapp dev server) +run-webapp: @which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1) @cd webapp && bun --bun node_modules/.bin/vite --mode development -.PHONY: run -run: ### Executes the project build locally - @make build - ${BUILDS_PATH}/${PROJECT_NAME} - -.PHONY: build-docker -build-docker: build-webapp ### Build Docker image locally using goreleaser snapshot - $(info: Make: Building Docker image) - @which goreleaser > /dev/null || (echo "Error: goreleaser is not installed. Install it from https://goreleaser.com" && exit 1) - @which docker > /dev/null || (echo "Error: docker is not installed" && exit 1) - @goreleaser --clean --snapshot --skip=publish - +## format: Format code and tidy modules .PHONY: format -format: ### Executes the formatting pipeline on the project - $(info: Make: Format) - @go fmt ./... - @go mod tidy +format: + go fmt ./... + go mod tidy +## ci-lint: Run golangci-lint (installs if missing) .PHONY: ci-lint -ci-lint: ### Check the project for errors - $(info: Make: Lint) - @go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} - @golangci-lint run ./... +ci-lint: + @which golangci-lint > /dev/null 2>&1 || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + golangci-lint run ./... +## lint: Run all linters .PHONY: lint -lint: lint-server lint-webapp ### Check the project for errors - $(info: Make: Lint) +lint: lint-server lint-webapp +## lint-server: Check the server for linting errors .PHONY: lint-server -lint-server: ### Check the server for linting errors - $(info: Make: Lint server) +lint-server: @golangci-lint run ./... +## lint-webapp: Check the webapp for linting errors .PHONY: lint-webapp -lint-webapp: ### Check the webapp for linting errors - $(info: Make: Lint webapp) +lint-webapp: @which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1) @cd webapp && bun --bun node_modules/.bin/eslint . +## test: Run tests with coverage .PHONY: test -test: ### Runs the test suite - $(info: Make: Test) - CGO_ENABLED=1 go test ${TEST_OPTIONS} -timeout=${TEST_TIMEOUT} ${SOURCE_FILES} - @echo "Generating HTML coverage report..." - @go tool cover -html=coverage.out -o coverage.html - @echo "โœ… Coverage report generated: coverage.html" +test: + CGO_ENABLED=1 go test $(TEST_OPTIONS) -timeout=$(TEST_TIMEOUT) ./... +## e2e: Run end-to-end integration tests .PHONY: e2e -e2e: ### Run end-to-end integration tests - $(info: Make: E2E Tests) +e2e: @./scripts/e2e.sh +## e2e-install: Install Playwright browsers .PHONY: e2e-install -e2e-install: ### Install Playwright browsers - $(info: Make: Installing E2E dependencies) +e2e-install: @go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium diff --git a/docker-compose.yml b/docker-compose.yml index 7cb0e56..912e762 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,10 @@ services: hako: - # Note: Before running docker-compose, ensure the Linux binary is prepared: - # Run: make prepare-docker-compose - # Or: make build-docker (which builds via goreleaser) + # Note: Build the Docker image via goreleaser: + # Run: make build-docker build: context: . dockerfile: Containerfile - platforms: - - linux/amd64 - args: - TARGETPLATFORM: dist/hako_linux_amd64_v1 - platform: linux/amd64 container_name: hako restart: unless-stopped ports: diff --git a/e2e/Dockerfile.e2e b/e2e/Dockerfile.e2e index f8ad861..90fbc2d 100644 --- a/e2e/Dockerfile.e2e +++ b/e2e/Dockerfile.e2e @@ -1,4 +1,4 @@ -FROM alpine:3.20 +FROM alpine:3.23 RUN apk add --no-cache poppler-utils ca-certificates tzdata && \ addgroup -g 1000 hako && \ diff --git a/e2e/e2eutil/container.go b/e2e/e2eutil/container.go index 700821d..f5c8fc1 100644 --- a/e2e/e2eutil/container.go +++ b/e2e/e2eutil/container.go @@ -53,16 +53,23 @@ func StartHakoContainer(ctx context.Context, t *testing.T) (testcontainers.Conta os.Remove(binaryPath) }) - // Create container request + // Ensure shared network exists so Hako can reach the test web server by alias. + nw, err := getSharedNetwork(ctx) + if err != nil { + return nil, "", err + } + + // Create container request attached to the shared network. req := testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ - Context: filepath.Join(projectRoot, "e2e"), - Dockerfile: "Dockerfile.e2e", + Context: filepath.Join(projectRoot, "e2e"), + Dockerfile: "Dockerfile.e2e", PrintBuildLog: true, }, ExposedPorts: []string{"8080/tcp"}, WaitingFor: wait.ForLog("Starting HTTP server"). WithStartupTimeout(30 * time.Second), + Networks: []string{nw.Name}, } // Start container diff --git a/e2e/e2eutil/helper.go b/e2e/e2eutil/helper.go index aef2371..23cea02 100644 --- a/e2e/e2eutil/helper.go +++ b/e2e/e2eutil/helper.go @@ -178,10 +178,12 @@ func (h *TestHelper) GetURL() string { return h.page.URL() } -// IsVisible checks if an element matching the selector is visible. +// IsVisible checks if any element matching the selector is visible. +// Uses .First() to avoid Playwright strict-mode errors when the selector +// resolves to multiple elements. func (h *TestHelper) IsVisible(selector string) bool { h.t.Helper() - visible, err := h.page.Locator(selector).IsVisible() + visible, err := h.page.Locator(selector).First().IsVisible() if err != nil { h.t.Logf("Error checking visibility of %s: %v", selector, err) return false @@ -305,9 +307,18 @@ func (h *TestHelper) Logout() error { } // CreateLink creates a new link via the Quick Add form. +// Navigates to /home first to ensure the form is available even when called +// repeatedly (after the first call the user is redirected to /links). func (h *TestHelper) CreateLink(url string) error { h.t.Helper() + // Ensure we're on the home page where the Quick Add form lives. + if !strings.HasSuffix(h.GetURL(), "/home") { + if err := h.Navigate("/home"); err != nil { + return fmt.Errorf("failed to navigate to home: %w", err) + } + } + // Wait for URL input on home page if err := h.WaitForSelector("input[name=\"url\"]", 5*time.Second); err != nil { return fmt.Errorf("URL input not found: %w", err) diff --git a/e2e/e2eutil/network.go b/e2e/e2eutil/network.go new file mode 100644 index 0000000..7200d65 --- /dev/null +++ b/e2e/e2eutil/network.go @@ -0,0 +1,34 @@ +package e2eutil + +import ( + "context" + "fmt" + "sync" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" +) + +// Shared network used by the test web server and Hako container so that +// Hako can fetch URLs from the test web server via Docker DNS. +var ( + sharedNetwork *testcontainers.DockerNetwork + sharedNetworkErr error + sharedNetworkOnce sync.Once +) + +// Network alias used by the test web server inside the shared network. +const testServerAlias = "hako-testserver" + +// getSharedNetwork lazily creates a shared Docker network for the test process. +// Both StartTestWebServer and StartHakoContainer attach to it so containers +// can communicate by alias. +func getSharedNetwork(ctx context.Context) (*testcontainers.DockerNetwork, error) { + sharedNetworkOnce.Do(func() { + sharedNetwork, sharedNetworkErr = network.New(ctx) + }) + if sharedNetworkErr != nil { + return nil, fmt.Errorf("failed to create shared network: %w", sharedNetworkErr) + } + return sharedNetwork, nil +} diff --git a/e2e/e2eutil/testserver.go b/e2e/e2eutil/testserver.go index c8d5d7a..11a78d9 100644 --- a/e2e/e2eutil/testserver.go +++ b/e2e/e2eutil/testserver.go @@ -7,13 +7,13 @@ import ( "testing" "time" - "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) // StartTestWebServer starts an nginx container with test content for E2E testing. -// It builds the test web server image and returns the container instance and base URL. +// The container is attached to the shared network with the testServerAlias hostname, +// and the returned URL uses that alias so the Hako container can fetch from it. func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Container, string, error) { t.Helper() @@ -23,7 +23,13 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta return nil, "", fmt.Errorf("failed to get project root: %w", err) } - // Create container request + // Ensure shared network exists so Hako can reach this server by alias. + nw, err := getSharedNetwork(ctx) + if err != nil { + return nil, "", err + } + + // Create container request attached to the shared network. req := testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join(projectRoot, "e2e", "testserver"), @@ -32,6 +38,10 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta }, ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), + Networks: []string{nw.Name}, + NetworkAliases: map[string][]string{ + nw.Name: {testServerAlias}, + }, } // Start container @@ -51,21 +61,10 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta } }) - // Get the mapped port - mappedPort, err := container.MappedPort(ctx, nat.Port("80/tcp")) - if err != nil { - return nil, "", fmt.Errorf("failed to get mapped port: %w", err) - } - - // Get the host - host, err := container.Host(ctx) - if err != nil { - return nil, "", fmt.Errorf("failed to get container host: %w", err) - } - - // Construct base URL - baseURL := fmt.Sprintf("http://%s:%s", host, mappedPort.Port()) - t.Logf("Test web server started at %s", baseURL) + // Use the network alias as the base URL so Hako can reach this server + // from inside its container via Docker DNS. + baseURL := fmt.Sprintf("http://%s", testServerAlias) + t.Logf("Test web server started at %s (network alias)", baseURL) return container, baseURL, nil } diff --git a/e2e/go.mod b/e2e/go.mod index 8d6b77a..1972dd1 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -6,6 +6,8 @@ replace github.com/fmartingr/hako => ../ require ( github.com/docker/go-connections v0.6.0 + github.com/google/uuid v1.6.0 + github.com/mattn/go-sqlite3 v1.14.41 github.com/playwright-community/playwright-go v0.5200.1 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 @@ -34,7 +36,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 7b98b1b..c2ceab7 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -68,6 +68,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA= +github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= diff --git a/e2e/playwright/admin_rules_test.go b/e2e/playwright/admin_rules_test.go index 9074391..d613bdc 100644 --- a/e2e/playwright/admin_rules_test.go +++ b/e2e/playwright/admin_rules_test.go @@ -265,7 +265,7 @@ func TestRules_RuleApplication(t *testing.T) { helper.Screenshot("link-created-rule-test") // Navigate to link details to see which archiver was used - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") if err == nil { time.Sleep(1 * time.Second) helper.Screenshot("link-details-archiver-used") diff --git a/e2e/playwright/archives_test.go b/e2e/playwright/archives_test.go index 2199d01..e06afff 100644 --- a/e2e/playwright/archives_test.go +++ b/e2e/playwright/archives_test.go @@ -53,13 +53,14 @@ func TestArchive_AutomaticCreation(t *testing.T) { helper.Screenshot("link-created") // Navigate to link details to see archive status - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") require.NoError(t, err, "Failed to navigate to link details") - time.Sleep(1 * time.Second) helper.Screenshot("link-details-archive") - // Verify archive section exists - helper.AssertVisible(".archive-status, .archive-history, [class*='archive']", "Archive information should be visible") + // Wait for archive processing to surface in the UI + err = helper.WaitForSelector(".archive-status, [class*='archive']", 30*time.Second) + require.NoError(t, err, "Archive information should appear") + helper.AssertVisible(".archive-status, [class*='archive']", "Archive information should be visible") t.Log("Archive automatic creation test completed successfully") } @@ -105,15 +106,15 @@ func TestArchive_ViewHistory(t *testing.T) { time.Sleep(2 * time.Second) // Navigate to link details - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") require.NoError(t, err, "Failed to navigate to link details") - time.Sleep(1 * time.Second) helper.Screenshot("archive-history") - // Verify archive history is visible - helper.AssertVisible(".archive-history, .archive-list, [class*='archive']", "Archive history should be visible") + // Wait for archive to appear in the UI + err = helper.WaitForSelector(".archive-status, [class*='archive']", 30*time.Second) + require.NoError(t, err, "Archive history should appear") + helper.AssertVisible(".archive-status, [class*='archive']", "Archive history should be visible") - // Wait for archive to process (with timeout) t.Log("Waiting for archive to complete...") time.Sleep(10 * time.Second) helper.Screenshot("archive-after-wait") @@ -162,7 +163,7 @@ func TestArchive_StatusDisplay(t *testing.T) { time.Sleep(2 * time.Second) // Navigate to link details - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") require.NoError(t, err, "Failed to navigate to link details") time.Sleep(1 * time.Second) helper.Screenshot("archive-status") @@ -222,7 +223,7 @@ func TestArchive_Rearchive(t *testing.T) { time.Sleep(2 * time.Second) // Navigate to link details - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") require.NoError(t, err, "Failed to navigate to link details") time.Sleep(1 * time.Second) helper.Screenshot("before-rearchive") @@ -295,7 +296,7 @@ func TestArchive_ViewFiles(t *testing.T) { time.Sleep(2 * time.Second) // Navigate to link details - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") require.NoError(t, err, "Failed to navigate to link details") time.Sleep(1 * time.Second) @@ -356,7 +357,7 @@ func TestArchive_PreviewHTML(t *testing.T) { time.Sleep(2 * time.Second) // Navigate to link details - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") require.NoError(t, err, "Failed to navigate to link details") time.Sleep(1 * time.Second) diff --git a/e2e/playwright/errors_test.go b/e2e/playwright/errors_test.go index 661302b..1b90913 100644 --- a/e2e/playwright/errors_test.go +++ b/e2e/playwright/errors_test.go @@ -144,7 +144,7 @@ func TestError_ArchivalFailure(t *testing.T) { helper.Screenshot("archival-failure-link-created") // Navigate to link details - err = helper.Click("a[href*='/links/']") + err = helper.Click(".link-item") if err == nil { time.Sleep(5 * time.Second) helper.Screenshot("archival-failure-details") diff --git a/scripts/e2e.sh b/scripts/e2e.sh index 770f21d..50c5d92 100755 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -4,19 +4,19 @@ set -e echo "๐Ÿงช Running Hako E2E Tests" # Environment setup -export TESTCONTAINERS_RYUK_DISABLED=false +export TESTCONTAINERS_RYUK_DISABLED="${TESTCONTAINERS_RYUK_DISABLED:-true}" export DOCKER_HOST="${DOCKER_HOST:-unix:///var/run/docker.sock}" # Check Docker if ! docker info > /dev/null 2>&1; then - echo "โŒ Error: Docker is not running" + echo "โŒ Error: Docker is not available (check DOCKER_HOST and socket mount)" exit 1 fi # Install Playwright if needed if ! go run github.com/playwright-community/playwright-go/cmd/playwright@latest --help > /dev/null 2>&1; then echo "๐Ÿ“ฆ Installing Playwright browsers..." - go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium + (cd e2e && go run github.com/playwright-community/playwright-go/cmd/playwright@latest install chromium) fi # Build webapp (required for embed) diff --git a/webapp/dist/.gitkeep b/webapp/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webapp/embed.go b/webapp/embed.go index b13cf76..d2e69b9 100644 --- a/webapp/embed.go +++ b/webapp/embed.go @@ -7,7 +7,7 @@ import ( "os" ) -//go:embed dist +//go:embed all:dist var embeddedWebappFS embed.FS // GetFilesystem returns a HTTP filesystem with the embedded webapp files