test: add self-contained integration environment
Separate schema migrations from environment-specific fixtures, run the full API workflow against a disposable PostgreSQL Testcontainer, and provide seeded development and UI test environments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
6153017b62
commit
6969f79015
18 changed files with 772 additions and 269 deletions
123
.github/copilot-instructions.md
vendored
123
.github/copilot-instructions.md
vendored
|
|
@ -5,97 +5,72 @@
|
||||||
Run repository-wide commands from the root:
|
Run repository-wide commands from the root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dev # build and run PostgreSQL, API, and frontend
|
make dev # run PostgreSQL, API, and frontend with development fixtures
|
||||||
make db-up # start only PostgreSQL
|
make test # complete suite, including disposable PostgreSQL integration tests
|
||||||
make migrate # apply Goose migrations
|
make test-integration # Go tests; starts its own PostgreSQL Testcontainer
|
||||||
make migrate-down # roll back one migration
|
make test-env # isolated seeded UI stack at http://localhost:15173
|
||||||
make test # backend and frontend unit/component tests
|
make test-env-down # remove the test stack and its PostgreSQL volume
|
||||||
make test-integration # PostgreSQL-backed repository tests; database must be running
|
|
||||||
make lint # go vet and ESLint
|
make lint # go vet and ESLint
|
||||||
make build # compile the API and production frontend
|
make build # compile the API and production frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Run focused tests from the owning application:
|
Focused tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend && go test ./internal/service -run '^TestCandidatesRequireSixtyPercent$'
|
cd backend && go test ./internal/service -run '^TestCandidatesRequireSixtyPercent$'
|
||||||
|
cd backend && go test -tags=integration ./internal/integration -run '^TestCompleteInterviewWorkflow$'
|
||||||
cd frontend && npm test -- src/App.test.tsx -t "filters jobs by title"
|
cd frontend && npm test -- src/App.test.tsx -t "filters jobs by title"
|
||||||
```
|
```
|
||||||
|
|
||||||
The required toolchain is Go 1.27.1 and React 19.3 with TypeScript/Vite. Frontend
|
The required toolchain is Go 1.27.1 and React 19.3 with TypeScript/Vite. Use
|
||||||
formatting can be checked with `cd frontend && npm run format:check`; backend files
|
`cd frontend && npm run format:check` for frontend formatting and `gofmt` for Go.
|
||||||
are formatted with `gofmt`.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The repository has two applications:
|
- `backend/cmd/api` wires configuration, PostgreSQL, security, services, handlers,
|
||||||
|
and the Chi router. Requests flow through `internal/web/{router,handler,model,mapper}`
|
||||||
|
to `internal/service/{model,services}` and then
|
||||||
|
`internal/database/{repository,mapper,entity}`. Never expose GORM entities from
|
||||||
|
handlers.
|
||||||
|
- `backend/cmd/seed` applies embedded environment-aware fixtures.
|
||||||
|
- `frontend/src/App.tsx` owns routing/layout/screens, `src/api.ts` is the typed HTTP
|
||||||
|
boundary, and `src/types.ts` contains client domain types. Vite proxies `/api` to
|
||||||
|
the Go service in development.
|
||||||
|
|
||||||
- `backend/` is a Go HTTP API. `cmd/api` wires configuration, PostgreSQL, security,
|
Goose files in `backend/migrations` are schema-only and run in every environment.
|
||||||
services, handlers, and routing. Requests flow through
|
`internal/database/seed/common.sql` contains shared reference data;
|
||||||
`internal/web/{router,handler,model,mapper}` to
|
`development.sql` and `test.sql` contain idempotent environment-specific fixtures.
|
||||||
`internal/service/{model,services}` and then
|
Every entity change requires a migration plus corresponding fixture and integration
|
||||||
`internal/database/{repository,mapper,entity}`. Do not bypass these mapping
|
test updates. Never put demo users or test records in schema migrations.
|
||||||
boundaries by exposing GORM entities from handlers.
|
|
||||||
- `frontend/` is a React SPA. `src/App.tsx` owns routing/layout/screens,
|
|
||||||
`src/api.ts` is the typed HTTP boundary, and `src/types.ts` contains client domain
|
|
||||||
types. Vite proxies `/api` to the Go service in development.
|
|
||||||
|
|
||||||
Goose SQL files in `backend/migrations/` are the database schema source of truth.
|
Authentication uses bcrypt passwords and HMAC-signed bearer JWTs. Domain routes are
|
||||||
The initial migration also supplies representative jobs, questions, participants,
|
protected except `/api/health`, `/api/auth/login`, and opaque-token participant
|
||||||
traits, and the local interviewer account.
|
interview routes. Never expose password hashes, CV/photo bytes, or interviewer
|
||||||
|
reference answers in public JSON.
|
||||||
|
|
||||||
Authentication is custom bearer-token authentication: passwords are bcrypt hashes,
|
Questions are permanent reusable records linked to jobs through
|
||||||
`POST /api/auth/login` issues an HMAC-signed JWT, and all domain routes require that
|
`job_posting_questions`; detach links rather than deleting questions. Deprecated
|
||||||
token. Never return password hashes or participant photo/CV bytes in JSON; those
|
questions remain searchable but cannot be newly attached. Question values are
|
||||||
files have dedicated endpoints.
|
`open`, `multiple_choice`, `radio`, and `code_review`. Choice types require at least
|
||||||
|
two options. Open/code-review types require `referenceAnswer`; code review also
|
||||||
|
requires `codeSnippet`.
|
||||||
|
|
||||||
CV ingestion stores the original bytes and extracts text from plain text, PDF, or
|
Jobs move through `draft`, `published`, and `deprecated`. Publishing requires a
|
||||||
DOCX input. Trait extraction only considers normalized traits already present in
|
question, only published jobs can create invitations, and candidate matching uses
|
||||||
the database. Candidate matching compares a participant's extracted traits with a
|
labels plus both skill groups with a 60% threshold. Invitation URLs use
|
||||||
job's labels, required skills, and additional skills; include matches only at the
|
`/participant/:invitationId`, where the identifier is an opaque token. Participant
|
||||||
`service.CandidateThreshold` of 60% or higher.
|
questions are acceptance-gated and answers are saved before final submission;
|
||||||
|
interviewer review uses
|
||||||
Questions are permanent, reusable library records and jobs own many-to-many links
|
`GET /api/jobs/{id}/invitations/{invitationId}`.
|
||||||
to them. Detaching removes only the link. The lifecycle is
|
|
||||||
`draft -> published -> deprecated`; publishing requires at least one question, and
|
|
||||||
only published jobs can create invitations. Once an invitation exists, its question
|
|
||||||
set cannot be changed, and its duration is snapshotted so later job edits do not
|
|
||||||
alter an active interview.
|
|
||||||
|
|
||||||
Invitation URLs expose an opaque UUID token as `/participant/:invitationId` and use
|
|
||||||
public `/api/interviews/{token}` API routes; they do not use interviewer JWT
|
|
||||||
authentication. An invitation progresses from
|
|
||||||
`pending` to `accepted` to `completed`. Answers may only be saved while accepted
|
|
||||||
and before the snapshotted timer expires. The participant UI must gate questions
|
|
||||||
behind explicit acceptance and provide First, Previous, Next, Last, and Submit
|
|
||||||
controls. Interviewer review uses the protected
|
|
||||||
`GET /api/jobs/{id}/invitations/{invitationId}` endpoint, which intentionally
|
|
||||||
includes reference answers; the public interview endpoint does not.
|
|
||||||
|
|
||||||
## Repository conventions
|
## Repository conventions
|
||||||
|
|
||||||
- Every Go interface name starts with `I` (`IStore`, `IJobService`,
|
- Prefix every Go interface name with `I`.
|
||||||
`IParticipantService`, `ITokenManager`).
|
- Keep web, service, and persistence models separate and convert them in adjacent
|
||||||
- Keep transport, service, and persistence structs separate. Conversion belongs in
|
`mapper` packages.
|
||||||
the adjacent `mapper` package, not in handlers or GORM entities.
|
- Participant creation uses multipart fields `firstName`, `lastName`, `birthday`,
|
||||||
- Question types are the exact API values `open`, `multiple_choice`, `radio`, and
|
`email`, `contactInfo`, `photo`, and `cv`.
|
||||||
`code_review`. Multiple-choice and radio questions require at least two options.
|
- Return explicit errors; map expected failures to HTTP status codes and log
|
||||||
- Never delete question records through job operations. Use
|
unexpected failures instead of silently falling back.
|
||||||
`job_posting_questions` to attach/detach, and use the `deprecated` flag to retire
|
- Commit every repository change with a meaningful conventional prefix such as
|
||||||
library questions. Full question edits use `PUT /api/questions/{id}`; deprecation
|
`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, or `chore:`.
|
||||||
remains a separate `PATCH`.
|
|
||||||
- The question create/edit form builds multiple-choice and radio options one at a
|
|
||||||
time. Keep option editing state separate from the saved options array and require
|
|
||||||
at least two options for those types. Open and code-review questions instead
|
|
||||||
require a `referenceAnswer` textarea. Never include that interviewer-only answer
|
|
||||||
in public interview responses.
|
|
||||||
- Code-review questions also require `codeSnippet`. It is safe to include in the
|
|
||||||
participant payload and is rendered as a line-numbered pull-request review panel;
|
|
||||||
the participant's review comment remains the ordinary persisted answer.
|
|
||||||
- Traits are de-duplicated case-insensitively through their normalized database
|
|
||||||
value. Labels and both skill groups all reuse the same `traits` table.
|
|
||||||
- Participant creation is multipart form data using `firstName`, `lastName`,
|
|
||||||
`birthday`, `email`, `contactInfo`, `photo`, and `cv`.
|
|
||||||
- Return explicit errors. Handlers convert expected validation/not-found/authentication
|
|
||||||
failures to HTTP responses and log unexpected failures rather than silently
|
|
||||||
falling back to demo data.
|
|
||||||
|
|
|
||||||
26
.github/workflows/ci.yml
vendored
Normal file
26
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: backend/go.mod
|
||||||
|
cache-dependency-path: backend/go.sum
|
||||||
|
- uses: actions/setup-node@v5
|
||||||
|
with:
|
||||||
|
node-version: 26
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
- run: make test
|
||||||
|
- run: make lint
|
||||||
|
- run: make build
|
||||||
19
Makefile
19
Makefile
|
|
@ -4,7 +4,7 @@ DATABASE_URL ?= postgres://go_guess:go_guess@localhost:5432/go_guess?sslmode=dis
|
||||||
JWT_SECRET ?= local-development-secret-change-me-12345
|
JWT_SECRET ?= local-development-secret-change-me-12345
|
||||||
GOOSE := cd backend && go run github.com/pressly/goose/v3/cmd/goose@v3.26.0
|
GOOSE := cd backend && go run github.com/pressly/goose/v3/cmd/goose@v3.26.0
|
||||||
|
|
||||||
.PHONY: dev db-up db-down migrate migrate-down backend frontend test test-backend test-frontend test-integration lint build
|
.PHONY: dev db-up db-down migrate migrate-down seed backend frontend frontend-install test test-backend test-frontend test-integration test-env test-env-down lint build
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
|
|
@ -21,13 +21,19 @@ migrate:
|
||||||
migrate-down:
|
migrate-down:
|
||||||
$(GOOSE) -dir migrations postgres "$(DATABASE_URL)" down
|
$(GOOSE) -dir migrations postgres "$(DATABASE_URL)" down
|
||||||
|
|
||||||
|
seed:
|
||||||
|
cd backend && DATABASE_URL="$(DATABASE_URL)" APP_ENV="$${APP_ENV:-development}" go run ./cmd/seed
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
cd backend && DATABASE_URL="$(DATABASE_URL)" JWT_SECRET="$(JWT_SECRET)" go run ./cmd/api
|
cd backend && DATABASE_URL="$(DATABASE_URL)" JWT_SECRET="$(JWT_SECRET)" go run ./cmd/api
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
cd frontend && npm run dev
|
cd frontend && npm run dev
|
||||||
|
|
||||||
test: test-backend test-frontend
|
frontend-install:
|
||||||
|
cd frontend && npm ci
|
||||||
|
|
||||||
|
test: frontend-install test-integration test-frontend
|
||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
cd backend && go test ./...
|
cd backend && go test ./...
|
||||||
|
|
@ -36,7 +42,14 @@ test-frontend:
|
||||||
cd frontend && npm test
|
cd frontend && npm test
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
cd backend && TEST_DATABASE_URL="$(DATABASE_URL)" go test -tags=integration ./internal/database/repository
|
cd backend && go test -tags=integration ./...
|
||||||
|
|
||||||
|
test-env:
|
||||||
|
COMPOSE_PROJECT_NAME=go-guess-test APP_ENV=test DB_PORT=15432 API_PORT=18080 \
|
||||||
|
WEB_PORT=15173 FRONTEND_URL=http://localhost:15173 docker compose up -d --build
|
||||||
|
|
||||||
|
test-env-down:
|
||||||
|
COMPOSE_PROJECT_NAME=go-guess-test docker compose down -v --remove-orphans
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
cd backend && go vet ./...
|
cd backend && go vet ./...
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -29,6 +29,7 @@ To run the applications outside containers:
|
||||||
```bash
|
```bash
|
||||||
make db-up
|
make db-up
|
||||||
make migrate
|
make migrate
|
||||||
|
make seed
|
||||||
make backend
|
make backend
|
||||||
make frontend
|
make frontend
|
||||||
```
|
```
|
||||||
|
|
@ -39,8 +40,8 @@ outside local development.
|
||||||
## Quality commands
|
## Quality commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test
|
make test # full suite, including disposable PostgreSQL integration tests
|
||||||
make test-integration
|
make test-integration # Go integration suite with its own PostgreSQL container
|
||||||
make lint
|
make lint
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
@ -57,6 +58,19 @@ Run one frontend test:
|
||||||
cd frontend && npm test -- src/App.test.tsx -t "filters jobs by title"
|
cd frontend && npm test -- src/App.test.tsx -t "filters jobs by title"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Docker must be available for `make test`. The integration suite starts PostgreSQL
|
||||||
|
18 in a disposable Testcontainer, applies every Goose migration, loads deterministic
|
||||||
|
test fixtures, exercises login and the complete interview workflow, and removes the
|
||||||
|
container afterward.
|
||||||
|
|
||||||
|
Start an isolated application stack populated with test data for manual UI testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-env
|
||||||
|
# Open http://localhost:15173 and use integration@example.com / integration-password
|
||||||
|
make test-env-down
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Requests enter the Chi router and web handlers under `backend/internal/web`.
|
Requests enter the Chi router and web handlers under `backend/internal/web`.
|
||||||
|
|
@ -72,9 +86,12 @@ submission without requiring an interviewer login. Interviewers can review every
|
||||||
saved answer from the job's invitation list. Vite proxies `/api` to the Go service
|
saved answer from the job's invitation list. Vite proxies `/api` to the Go service
|
||||||
during local development.
|
during local development.
|
||||||
|
|
||||||
Goose migrations in `backend/migrations` are the schema source of truth and include
|
Goose migrations in `backend/migrations` are schema-only and run in every
|
||||||
representative local data. Uploaded CVs are stored as bytes; plain text, PDF, and
|
environment. Embedded SQL under `backend/internal/database/seed` separates shared
|
||||||
DOCX content is inspected for normalized traits already defined by job postings.
|
reference traits from idempotent development and test fixtures. Docker Compose
|
||||||
|
loads development fixtures by default; production loads only shared data. Uploaded
|
||||||
|
CVs are stored as bytes; plain text, PDF, and DOCX content is inspected for
|
||||||
|
normalized traits already defined by job postings.
|
||||||
|
|
||||||
Jobs move through `draft`, `published`, and `deprecated` states. Questions are
|
Jobs move through `draft`, `published`, and `deprecated` states. Questions are
|
||||||
editable, permanent library records: jobs attach and detach links rather than
|
editable, permanent library records: jobs attach and detach links rather than
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api \
|
||||||
|
&& CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/seed ./cmd/seed
|
||||||
|
|
||||||
FROM golang:1.27.1-alpine AS tools
|
FROM golang:1.27.1-alpine AS tools
|
||||||
RUN GOBIN=/out go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
|
RUN GOBIN=/out go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
|
||||||
|
|
@ -12,8 +13,9 @@ FROM alpine:3.22
|
||||||
RUN addgroup -S app && adduser -S app -G app
|
RUN addgroup -S app && adduser -S app -G app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /out/api /usr/local/bin/api
|
COPY --from=build /out/api /usr/local/bin/api
|
||||||
|
COPY --from=build /out/seed /usr/local/bin/seed
|
||||||
COPY --from=tools /out/goose /usr/local/bin/goose
|
COPY --from=tools /out/goose /usr/local/bin/goose
|
||||||
COPY migrations ./migrations
|
COPY migrations ./migrations
|
||||||
USER app
|
USER app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD ["sh", "-c", "goose -dir migrations postgres \"$DATABASE_URL\" up && exec api"]
|
CMD ["sh", "-c", "goose -dir migrations postgres \"$DATABASE_URL\" up && seed && exec api"]
|
||||||
|
|
|
||||||
33
backend/cmd/seed/main.go
Normal file
33
backend/cmd/seed/main.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/danyel/go-guess/backend/internal/database"
|
||||||
|
"github.com/danyel/go-guess/backend/internal/database/seed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
databaseURL := os.Getenv("DATABASE_URL")
|
||||||
|
if databaseURL == "" {
|
||||||
|
slog.Error("DATABASE_URL is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
db, err := database.Open(databaseURL)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("connect database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("get database connection", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer sqlDB.Close()
|
||||||
|
if err := seed.Apply(context.Background(), sqlDB, os.Getenv("APP_ENV")); err != nil {
|
||||||
|
slog.Error("seed database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,20 +6,73 @@ require (
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.7.5
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/ledongthuc/pdf v0.0.0-20260907135840-6c8c28e0e8a0
|
github.com/ledongthuc/pdf v0.0.0-20260907135840-6c8c28e0e8a0
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.54.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.7.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.10.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/stretchr/testify v1.11.0 // indirect
|
github.com/klauspost/compress v1.18.6 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/go-archive v0.2.0 // indirect
|
||||||
|
github.com/moby/moby/api v1.55.0 // indirect
|
||||||
|
github.com/moby/moby/client v0.5.0 // indirect
|
||||||
|
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||||
|
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||||
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/pressly/goose/v3 v3.26.0 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.6 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
github.com/testcontainers/testcontainers-go v0.44.0 // indirect
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
golang.org/x/text v0.40.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
116
backend/go.sum
116
backend/go.sum
|
|
@ -1,8 +1,46 @@
|
||||||
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||||
|
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
||||||
|
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||||
|
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|
@ -13,27 +51,105 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20260907135840-6c8c28e0e8a0 h1:7Q+xNAZFmnfYOMweHN3c/PDFUKKfY1pVJ26K++QvVfU=
|
github.com/ledongthuc/pdf v0.0.0-20260907135840-6c8c28e0e8a0 h1:7Q+xNAZFmnfYOMweHN3c/PDFUKKfY1pVJ26K++QvVfU=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20260907135840-6c8c28e0e8a0/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
|
github.com/ledongthuc/pdf v0.0.0-20260907135840-6c8c28e0e8a0/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||||
|
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||||
|
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||||
|
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||||
|
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||||
|
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
||||||
|
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
|
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
|
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||||
|
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||||
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
||||||
|
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0 h1:8fdv/9y3JMxjQ+ULAcOG8RtgeNu5t9XF9LolSXDuTwM=
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0/go.mod h1:CFr2LncGYokw+OKjXcr8ARCKG1SaC2UEnGxFBovE86g=
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||||
|
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||||
|
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||||
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|
|
||||||
9
backend/internal/database/seed/common.sql
Normal file
9
backend/internal/database/seed/common.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
INSERT INTO traits (name, normalized) VALUES
|
||||||
|
('Go', 'go'),
|
||||||
|
('PostgreSQL', 'postgresql'),
|
||||||
|
('Docker', 'docker'),
|
||||||
|
('React', 'react'),
|
||||||
|
('TypeScript', 'typescript'),
|
||||||
|
('Kubernetes', 'kubernetes'),
|
||||||
|
('Backend', 'backend')
|
||||||
|
ON CONFLICT (normalized) DO NOTHING;
|
||||||
87
backend/internal/database/seed/development.sql
Normal file
87
backend/internal/database/seed/development.sql
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
INSERT INTO users (email, password_hash, role)
|
||||||
|
VALUES ('interviewer@go-guess.local', crypt('admin123', gen_salt('bf')), 'interviewer')
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO job_postings (title, description, seniority, position, status, duration_minutes)
|
||||||
|
SELECT 'Senior Go Backend Engineer', 'Build reliable APIs for passenger-facing railway systems.',
|
||||||
|
'Senior', 'Backend Engineer', 'published', 60
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM job_postings WHERE title = 'Senior Go Backend Engineer');
|
||||||
|
|
||||||
|
INSERT INTO job_postings (title, description, seniority, position, status, duration_minutes)
|
||||||
|
SELECT 'Full-stack Product Engineer', 'Deliver accessible operational tools from database to browser.',
|
||||||
|
'Medior', 'Full-stack Engineer', 'published', 45
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM job_postings WHERE title = 'Full-stack Product Engineer');
|
||||||
|
|
||||||
|
INSERT INTO questions (text, type, reference_answer)
|
||||||
|
SELECT 'How do you design graceful shutdown for an HTTP service?', 'open',
|
||||||
|
'Stop accepting work, cancel dependent operations, drain active requests, and close resources within a deadline.'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM questions WHERE text = 'How do you design graceful shutdown for an HTTP service?');
|
||||||
|
|
||||||
|
INSERT INTO questions (text, type)
|
||||||
|
SELECT 'Which construct protects shared data across goroutines?', 'radio'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM questions WHERE text = 'Which construct protects shared data across goroutines?');
|
||||||
|
|
||||||
|
INSERT INTO questions (text, type, reference_answer, code_snippet)
|
||||||
|
SELECT 'Review this component and identify unnecessary renders.', 'code_review',
|
||||||
|
'Use a stable key and avoid recreating expensive derived values when the candidates input has not changed.',
|
||||||
|
E'function CandidateList({ candidates }) {\n return candidates.map((candidate, index) => (\n <CandidateCard key={index} candidate={candidate} />\n ));\n}'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM questions WHERE text = 'Review this component and identify unnecessary renders.');
|
||||||
|
|
||||||
|
INSERT INTO question_options (question_id, text)
|
||||||
|
SELECT q.id, option.text
|
||||||
|
FROM questions q
|
||||||
|
CROSS JOIN (VALUES ('sync.Mutex'), ('context.Context'), ('http.Client')) AS option(text)
|
||||||
|
WHERE q.text = 'Which construct protects shared data across goroutines?'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM question_options existing WHERE existing.question_id = q.id);
|
||||||
|
|
||||||
|
INSERT INTO job_posting_questions (job_posting_id, question_id)
|
||||||
|
SELECT j.id, q.id
|
||||||
|
FROM job_postings j
|
||||||
|
JOIN questions q ON q.text IN (
|
||||||
|
'How do you design graceful shutdown for an HTTP service?',
|
||||||
|
'Which construct protects shared data across goroutines?'
|
||||||
|
)
|
||||||
|
WHERE j.title = 'Senior Go Backend Engineer'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO job_posting_questions (job_posting_id, question_id)
|
||||||
|
SELECT j.id, q.id
|
||||||
|
FROM job_postings j
|
||||||
|
JOIN questions q ON q.text = 'Review this component and identify unnecessary renders.'
|
||||||
|
WHERE j.title = 'Full-stack Product Engineer'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO job_posting_labels (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized IN ('backend', 'go')
|
||||||
|
WHERE j.title = 'Senior Go Backend Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO job_required_skills (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized IN ('go', 'postgresql', 'docker')
|
||||||
|
WHERE j.title = 'Senior Go Backend Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO job_additional_skills (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized = 'kubernetes'
|
||||||
|
WHERE j.title = 'Senior Go Backend Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO job_posting_labels (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized IN ('react', 'typescript')
|
||||||
|
WHERE j.title = 'Full-stack Product Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO job_required_skills (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized IN ('go', 'react', 'typescript')
|
||||||
|
WHERE j.title = 'Full-stack Product Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO job_additional_skills (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized = 'docker'
|
||||||
|
WHERE j.title = 'Full-stack Product Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO participants (first_name, last_name, birthday, email, contact_info, cv, cv_filename)
|
||||||
|
VALUES
|
||||||
|
('Ada', 'Lovelace', '1990-12-10', 'ada@example.com', '+32 470 00 00 01',
|
||||||
|
convert_to('Senior backend engineer with Go, PostgreSQL and Docker experience.', 'UTF8'), 'ada-cv.txt'),
|
||||||
|
('Grace', 'Hopper', '1988-12-09', 'grace@example.com', '+32 470 00 00 02',
|
||||||
|
convert_to('Full-stack developer working with Go, React and TypeScript.', 'UTF8'), 'grace-cv.txt')
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO participant_traits (participant_id, trait_id)
|
||||||
|
SELECT p.id, t.id FROM participants p JOIN traits t ON t.normalized IN ('go', 'postgresql', 'docker')
|
||||||
|
WHERE p.email = 'ada@example.com' ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO participant_traits (participant_id, trait_id)
|
||||||
|
SELECT p.id, t.id FROM participants p JOIN traits t ON t.normalized IN ('go', 'react', 'typescript')
|
||||||
|
WHERE p.email = 'grace@example.com' ON CONFLICT DO NOTHING;
|
||||||
38
backend/internal/database/seed/seed.go
Normal file
38
backend/internal/database/seed/seed.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package seed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var files embed.FS
|
||||||
|
|
||||||
|
func Apply(ctx context.Context, db *sql.DB, environment string) error {
|
||||||
|
if _, err := apply(ctx, db, "common.sql"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch environment {
|
||||||
|
case "", "production":
|
||||||
|
return nil
|
||||||
|
case "development", "test":
|
||||||
|
_, err := apply(ctx, db, environment+".sql")
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported APP_ENV %q", environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(ctx context.Context, db *sql.DB, filename string) (sql.Result, error) {
|
||||||
|
content, err := files.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read seed %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
result, err := db.ExecContext(ctx, string(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("apply seed %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
40
backend/internal/database/seed/test.sql
Normal file
40
backend/internal/database/seed/test.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
INSERT INTO users (email, password_hash, role)
|
||||||
|
VALUES ('integration@example.com', crypt('integration-password', gen_salt('bf')), 'interviewer')
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO job_postings (title, description, seniority, position, status, duration_minutes)
|
||||||
|
SELECT 'Integration Backend Engineer', 'Deterministic job fixture for API and UI integration tests.',
|
||||||
|
'Senior', 'Backend Engineer', 'published', 30
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM job_postings WHERE title = 'Integration Backend Engineer');
|
||||||
|
|
||||||
|
INSERT INTO questions (text, type, reference_answer)
|
||||||
|
SELECT 'Explain graceful shutdown.', 'open',
|
||||||
|
'Stop new work, cancel dependencies, drain requests, and close resources.'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM questions WHERE text = 'Explain graceful shutdown.');
|
||||||
|
|
||||||
|
INSERT INTO questions (text, type, reference_answer, code_snippet)
|
||||||
|
SELECT 'Review the error handling.', 'code_review', 'Return or log the error instead of discarding it.',
|
||||||
|
E'func save() error {\n persist()\n return nil\n}'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM questions WHERE text = 'Review the error handling.');
|
||||||
|
|
||||||
|
INSERT INTO job_posting_questions (job_posting_id, question_id)
|
||||||
|
SELECT j.id, q.id FROM job_postings j
|
||||||
|
JOIN questions q ON q.text IN ('Explain graceful shutdown.', 'Review the error handling.')
|
||||||
|
WHERE j.title = 'Integration Backend Engineer'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO job_posting_labels (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized = 'backend'
|
||||||
|
WHERE j.title = 'Integration Backend Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO job_required_skills (job_posting_id, trait_id)
|
||||||
|
SELECT j.id, t.id FROM job_postings j JOIN traits t ON t.normalized IN ('go', 'postgresql')
|
||||||
|
WHERE j.title = 'Integration Backend Engineer' ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO participants (first_name, last_name, birthday, email, contact_info, cv, cv_filename)
|
||||||
|
VALUES ('Integration', 'Candidate', '1992-04-15', 'candidate.integration@example.com',
|
||||||
|
'+32 470 00 00 99', convert_to('Backend engineer using Go and PostgreSQL.', 'UTF8'), 'integration-cv.txt')
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO participant_traits (participant_id, trait_id)
|
||||||
|
SELECT p.id, t.id FROM participants p JOIN traits t ON t.normalized IN ('backend', 'go', 'postgresql')
|
||||||
|
WHERE p.email = 'candidate.integration@example.com' ON CONFLICT DO NOTHING;
|
||||||
272
backend/internal/integration/api_test.go
Normal file
272
backend/internal/integration/api_test.go
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
"github.com/testcontainers/testcontainers-go"
|
||||||
|
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||||
|
"github.com/testcontainers/testcontainers-go/wait"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/danyel/go-guess/backend/internal/database/repository"
|
||||||
|
"github.com/danyel/go-guess/backend/internal/database/seed"
|
||||||
|
"github.com/danyel/go-guess/backend/internal/security"
|
||||||
|
"github.com/danyel/go-guess/backend/internal/service"
|
||||||
|
"github.com/danyel/go-guess/backend/internal/web/handler"
|
||||||
|
webmodel "github.com/danyel/go-guess/backend/internal/web/model"
|
||||||
|
"github.com/danyel/go-guess/backend/internal/web/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testServer *httptest.Server
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
container, err := tcpostgres.Run(
|
||||||
|
ctx,
|
||||||
|
"postgres:18-alpine",
|
||||||
|
tcpostgres.WithDatabase("go_guess_test"),
|
||||||
|
tcpostgres.WithUsername("go_guess"),
|
||||||
|
tcpostgres.WithPassword("go_guess"),
|
||||||
|
testcontainers.WithWaitStrategy(
|
||||||
|
wait.ForLog("database system is ready to accept connections").
|
||||||
|
WithOccurrence(2).
|
||||||
|
WithStartupTimeout(60*time.Second),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "start PostgreSQL test container:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseURL, err := container.ConnectionString(ctx, "sslmode=disable")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "get PostgreSQL connection string:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
sqlDB, err := sql.Open("pgx", databaseURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "open PostgreSQL:", err)
|
||||||
|
_ = container.Terminate(context.Background())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := goose.SetDialect("postgres"); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "set Goose dialect:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
migrations := filepath.Join(filepath.Dir(filename), "..", "..", "migrations")
|
||||||
|
if err := goose.Up(sqlDB, migrations); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "apply migrations:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := seed.Apply(ctx, sqlDB, "test"); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "apply test seed:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := seed.Apply(ctx, sqlDB, "test"); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "reapply idempotent test seed:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gormDB, err := gorm.Open(postgres.Open(databaseURL), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "open GORM connection:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
store := repository.New(gormDB)
|
||||||
|
tokens := security.NewTokenManager("integration-test-secret-at-least-32-bytes", time.Hour)
|
||||||
|
invitations := service.NewInvitationService(store, "http://example.test")
|
||||||
|
testServer = httptest.NewServer(router.New(
|
||||||
|
handler.New(
|
||||||
|
service.NewAuthService(store, tokens),
|
||||||
|
service.NewJobService(store),
|
||||||
|
service.NewQuestionService(store),
|
||||||
|
service.NewParticipantService(store),
|
||||||
|
invitations,
|
||||||
|
10,
|
||||||
|
"http://example.test",
|
||||||
|
),
|
||||||
|
tokens,
|
||||||
|
"http://example.test",
|
||||||
|
))
|
||||||
|
|
||||||
|
code := m.Run()
|
||||||
|
testServer.Close()
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
_ = container.Terminate(cleanupCtx)
|
||||||
|
cleanupCancel()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteInterviewWorkflow(t *testing.T) {
|
||||||
|
var login webmodel.LoginResponse
|
||||||
|
requestJSON(t, http.MethodPost, "/api/auth/login", "", map[string]any{
|
||||||
|
"email": "integration@example.com", "password": "integration-password",
|
||||||
|
}, http.StatusOK, &login)
|
||||||
|
if login.Token == "" {
|
||||||
|
t.Fatal("login did not return a token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var participants []webmodel.ParticipantResponse
|
||||||
|
requestJSON(t, http.MethodGet, "/api/participants", login.Token, nil, http.StatusOK, &participants)
|
||||||
|
var participantID uint
|
||||||
|
for _, participant := range participants {
|
||||||
|
if participant.Email == "candidate.integration@example.com" {
|
||||||
|
participantID = participant.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if participantID == 0 {
|
||||||
|
t.Fatal("test participant was not seeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
var question webmodel.QuestionResponse
|
||||||
|
requestJSON(t, http.MethodPost, "/api/questions", login.Token, map[string]any{
|
||||||
|
"text": "Review this error handling.", "type": "code_review",
|
||||||
|
"codeSnippet": "func run() error {\n execute()\n return nil\n}",
|
||||||
|
"referenceAnswer": "Return the execute error.", "options": []string{},
|
||||||
|
}, http.StatusCreated, &question)
|
||||||
|
requestJSON(t, http.MethodPut, fmt.Sprintf("/api/questions/%d", question.ID), login.Token, map[string]any{
|
||||||
|
"text": "Review this ignored error.", "type": "code_review",
|
||||||
|
"codeSnippet": "func run() error {\n execute()\n return nil\n}",
|
||||||
|
"referenceAnswer": "Propagate the execute error.", "options": []string{},
|
||||||
|
}, http.StatusOK, &question)
|
||||||
|
|
||||||
|
var jobs []webmodel.JobResponse
|
||||||
|
requestJSON(t, http.MethodGet, "/api/jobs", login.Token, nil, http.StatusOK, &jobs)
|
||||||
|
var jobID uint
|
||||||
|
for _, job := range jobs {
|
||||||
|
if job.Title == "Integration Backend Engineer" {
|
||||||
|
jobID = job.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if jobID == 0 {
|
||||||
|
t.Fatal("test job was not seeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestJSON(t, http.MethodPost,
|
||||||
|
fmt.Sprintf("/api/jobs/%d/questions/%d", jobID, question.ID),
|
||||||
|
login.Token, nil, http.StatusNoContent, nil)
|
||||||
|
|
||||||
|
var matches []webmodel.CandidateResponse
|
||||||
|
requestJSON(t, http.MethodGet, fmt.Sprintf("/api/jobs/%d/candidates", jobID),
|
||||||
|
login.Token, nil, http.StatusOK, &matches)
|
||||||
|
if len(matches) == 0 || matches[0].Participant.ID != participantID {
|
||||||
|
t.Fatalf("seeded participant was not a candidate: %#v", matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
var invitation webmodel.InvitationResponse
|
||||||
|
requestJSON(t, http.MethodPost, fmt.Sprintf("/api/jobs/%d/invitations", jobID),
|
||||||
|
login.Token, map[string]any{"participantId": participantID}, http.StatusCreated, &invitation)
|
||||||
|
if invitation.Token == "" || invitation.ParticipantURL != "http://example.test/participant/"+invitation.Token {
|
||||||
|
t.Fatalf("unexpected invitation: %#v", invitation)
|
||||||
|
}
|
||||||
|
|
||||||
|
var welcome webmodel.InterviewResponse
|
||||||
|
publicBody := requestJSON(t, http.MethodGet, "/api/interviews/"+invitation.Token,
|
||||||
|
"", nil, http.StatusOK, &welcome)
|
||||||
|
if bytes.Contains(publicBody, []byte("referenceAnswer")) || welcome.Invitation.Status != "pending" {
|
||||||
|
t.Fatalf("invalid public welcome response: %s", publicBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var started webmodel.InterviewResponse
|
||||||
|
requestJSON(t, http.MethodPost, "/api/interviews/"+invitation.Token+"/accept",
|
||||||
|
"", nil, http.StatusOK, &started)
|
||||||
|
if started.Invitation.Status != "accepted" || started.Invitation.AcceptedAt == nil {
|
||||||
|
t.Fatalf("interview was not accepted: %#v", started.Invitation)
|
||||||
|
}
|
||||||
|
|
||||||
|
answer := "The execute error must be returned to the caller."
|
||||||
|
requestJSON(t, http.MethodPut,
|
||||||
|
fmt.Sprintf("/api/interviews/%s/answers/%d", invitation.Token, question.ID),
|
||||||
|
"", map[string]any{"answer": answer}, http.StatusNoContent, nil)
|
||||||
|
requestJSON(t, http.MethodPost, "/api/interviews/"+invitation.Token+"/finish",
|
||||||
|
"", nil, http.StatusOK, &started)
|
||||||
|
if started.Invitation.Status != "completed" {
|
||||||
|
t.Fatalf("interview was not completed: %#v", started.Invitation)
|
||||||
|
}
|
||||||
|
|
||||||
|
var review webmodel.InvitationReviewResponse
|
||||||
|
requestJSON(t, http.MethodGet,
|
||||||
|
fmt.Sprintf("/api/jobs/%d/invitations/%d", jobID, invitation.ID),
|
||||||
|
login.Token, nil, http.StatusOK, &review)
|
||||||
|
if review.Answers[question.ID] != answer {
|
||||||
|
t.Fatalf("saved answer missing from review: %#v", review.Answers)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, item := range review.Job.Questions {
|
||||||
|
if item.ID == question.ID {
|
||||||
|
found = item.ReferenceAnswer == "Propagate the execute error." && item.CodeSnippet != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("review did not include interviewer question context: %#v", review.Job.Questions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestJSON(
|
||||||
|
t *testing.T,
|
||||||
|
method string,
|
||||||
|
path string,
|
||||||
|
token string,
|
||||||
|
body any,
|
||||||
|
wantStatus int,
|
||||||
|
target any,
|
||||||
|
) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var requestBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
encoded, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
requestBody = bytes.NewReader(encoded)
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest(method, testServer.URL+path, requestBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
if token != "" {
|
||||||
|
request.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
content, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != wantStatus {
|
||||||
|
t.Fatalf("%s %s: expected %d, got %d: %s", method, path, wantStatus, response.StatusCode, content)
|
||||||
|
}
|
||||||
|
if target != nil {
|
||||||
|
if err := json.Unmarshal(content, target); err != nil {
|
||||||
|
t.Fatalf("decode %s %s: %v: %s", method, path, err, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
@ -80,49 +80,6 @@ CREATE TABLE participant_traits (
|
||||||
PRIMARY KEY (participant_id, trait_id)
|
PRIMARY KEY (participant_id, trait_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO users (email, password_hash, role)
|
|
||||||
VALUES ('interviewer@go-guess.local', crypt('admin123', gen_salt('bf')), 'interviewer');
|
|
||||||
|
|
||||||
INSERT INTO traits (name, normalized) VALUES
|
|
||||||
('Go', 'go'), ('PostgreSQL', 'postgresql'), ('Docker', 'docker'),
|
|
||||||
('React', 'react'), ('TypeScript', 'typescript'), ('Kubernetes', 'kubernetes'),
|
|
||||||
('Backend', 'backend');
|
|
||||||
|
|
||||||
INSERT INTO job_postings (title, description, seniority, position) VALUES
|
|
||||||
('Senior Go Backend Engineer', 'Build reliable APIs for passenger-facing railway systems.', 'Senior', 'Backend Engineer'),
|
|
||||||
('Full-stack Product Engineer', 'Deliver accessible operational tools from database to browser.', 'Medior', 'Full-stack Engineer');
|
|
||||||
|
|
||||||
INSERT INTO job_posting_labels (job_posting_id, trait_id)
|
|
||||||
SELECT 1, id FROM traits WHERE normalized IN ('backend', 'go')
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
INSERT INTO job_required_skills (job_posting_id, trait_id)
|
|
||||||
SELECT 1, id FROM traits WHERE normalized IN ('go', 'postgresql', 'docker');
|
|
||||||
INSERT INTO job_additional_skills (job_posting_id, trait_id)
|
|
||||||
SELECT 1, id FROM traits WHERE normalized = 'kubernetes';
|
|
||||||
|
|
||||||
INSERT INTO job_posting_labels (job_posting_id, trait_id)
|
|
||||||
SELECT 2, id FROM traits WHERE normalized IN ('react', 'typescript');
|
|
||||||
INSERT INTO job_required_skills (job_posting_id, trait_id)
|
|
||||||
SELECT 2, id FROM traits WHERE normalized IN ('go', 'react', 'typescript');
|
|
||||||
INSERT INTO job_additional_skills (job_posting_id, trait_id)
|
|
||||||
SELECT 2, id FROM traits WHERE normalized = 'docker';
|
|
||||||
|
|
||||||
INSERT INTO questions (job_id, text, type) VALUES
|
|
||||||
(1, 'How do you design graceful shutdown for an HTTP service?', 'open'),
|
|
||||||
(1, 'Which construct protects shared data across goroutines?', 'radio'),
|
|
||||||
(2, 'Review this component and identify unnecessary renders.', 'code_review');
|
|
||||||
INSERT INTO question_options (question_id, text) VALUES
|
|
||||||
(2, 'sync.Mutex'), (2, 'context.Context'), (2, 'http.Client');
|
|
||||||
|
|
||||||
INSERT INTO participants (first_name, last_name, birthday, email, contact_info, cv, cv_filename) VALUES
|
|
||||||
('Ada', 'Lovelace', '1990-12-10', 'ada@example.com', '+32 470 00 00 01', encode('Senior backend engineer with Go, PostgreSQL and Docker experience.', 'escape')::bytea, 'ada-cv.txt'),
|
|
||||||
('Grace', 'Hopper', '1988-12-09', 'grace@example.com', '+32 470 00 00 02', encode('Full-stack developer working with Go, React and TypeScript.', 'escape')::bytea, 'grace-cv.txt');
|
|
||||||
|
|
||||||
INSERT INTO participant_traits (participant_id, trait_id)
|
|
||||||
SELECT 1, id FROM traits WHERE normalized IN ('go', 'postgresql', 'docker');
|
|
||||||
INSERT INTO participant_traits (participant_id, trait_id)
|
|
||||||
SELECT 2, id FROM traits WHERE normalized IN ('go', 'react', 'typescript');
|
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
DROP TABLE IF EXISTS participant_traits;
|
DROP TABLE IF EXISTS participant_traits;
|
||||||
DROP TABLE IF EXISTS job_additional_skills;
|
DROP TABLE IF EXISTS job_additional_skills;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,5 @@
|
||||||
ALTER TABLE questions
|
ALTER TABLE questions
|
||||||
ADD COLUMN reference_answer TEXT NOT NULL DEFAULT '';
|
ADD COLUMN reference_answer TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
UPDATE questions
|
|
||||||
SET reference_answer = CASE
|
|
||||||
WHEN type = 'open' THEN 'Use context cancellation, stop accepting work, drain active requests, and close dependencies within a deadline.'
|
|
||||||
WHEN type = 'code_review' THEN 'Identify unstable dependencies or state updates that cause avoidable renders and propose memoization only where measured.'
|
|
||||||
ELSE reference_answer
|
|
||||||
END;
|
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
ALTER TABLE questions DROP COLUMN reference_answer;
|
ALTER TABLE questions DROP COLUMN reference_answer;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,5 @@
|
||||||
ALTER TABLE questions
|
ALTER TABLE questions
|
||||||
ADD COLUMN code_snippet TEXT NOT NULL DEFAULT '';
|
ADD COLUMN code_snippet TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
UPDATE questions
|
|
||||||
SET code_snippet = E'function CandidateList({ candidates }) {\n return candidates.map(candidate => (\n <CandidateCard candidate={candidate} />\n ));\n}'
|
|
||||||
WHERE type = 'code_review' AND code_snippet = '';
|
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
ALTER TABLE questions DROP COLUMN code_snippet;
|
ALTER TABLE questions DROP COLUMN code_snippet;
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/danyel/go-guess/backend/internal/security"
|
|
||||||
"github.com/danyel/go-guess/backend/internal/service/model"
|
|
||||||
"github.com/danyel/go-guess/backend/internal/web/handler"
|
|
||||||
"github.com/danyel/go-guess/backend/internal/web/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockInvitationService struct{}
|
|
||||||
|
|
||||||
func (m mockInvitationService) List(ctx context.Context, jobID uint) ([]model.Invitation, error) {
|
|
||||||
return []model.Invitation{{ID: 1}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockInvitationService) Create(ctx context.Context, jobID, participantID uint) (model.Invitation, error) {
|
|
||||||
return model.Invitation{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockInvitationService) Review(ctx context.Context, jobID, invitationID uint) (model.Interview, error) {
|
|
||||||
return model.Interview{
|
|
||||||
Invitation: model.Invitation{ID: 1, ParticipantName: "Test", ParticipantEmail: "test@example.com", Status: "completed"},
|
|
||||||
Job: model.JobPosting{ID: 1, Title: "Job"},
|
|
||||||
Answers: map[uint]string{1: "My answer"},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockInvitationService) GetInterview(ctx context.Context, token string) (model.Interview, error) {
|
|
||||||
return model.Interview{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockInvitationService) Accept(ctx context.Context, token string) (model.Interview, error) {
|
|
||||||
return model.Interview{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockInvitationService) SaveAnswer(ctx context.Context, token string, questionID uint, answer string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mockInvitationService) Finish(ctx context.Context, token string) (model.Interview, error) {
|
|
||||||
return model.Interview{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockStubs struct{}
|
|
||||||
|
|
||||||
func (mockStubs) Login(context.Context, string, string) (string, model.User, error) {
|
|
||||||
return "", model.User{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mockStubs) List(context.Context) ([]model.JobPosting, error) {
|
|
||||||
return []model.JobPosting{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) Get(context.Context, uint) (model.JobPosting, error) {
|
|
||||||
return model.JobPosting{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) Create(context.Context, model.JobPosting) (model.JobPosting, error) {
|
|
||||||
return model.JobPosting{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) Update(context.Context, uint, string, int) (model.JobPosting, error) {
|
|
||||||
return model.JobPosting{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) AttachQuestion(context.Context, uint, uint) error { return nil }
|
|
||||||
func (mockStubs) DetachQuestion(context.Context, uint, uint) error { return nil }
|
|
||||||
func (mockStubs) Candidates(context.Context, uint) ([]model.CandidateMatch, error) {
|
|
||||||
return []model.CandidateMatch{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mockStubs) List2(context.Context) ([]model.Participant, error) { return nil, nil }
|
|
||||||
func (mockStubs) Get2(context.Context, uint) (model.Participant, error) {
|
|
||||||
return model.Participant{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) Create2(context.Context, model.Participant) (model.Participant, error) {
|
|
||||||
return model.Participant{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mockStubs) List3(context.Context, string) ([]model.Question, error) { return nil, nil }
|
|
||||||
func (mockStubs) Create3(context.Context, model.Question) (model.Question, error) {
|
|
||||||
return model.Question{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) Update3(context.Context, uint, model.Question) (model.Question, error) {
|
|
||||||
return model.Question{}, nil
|
|
||||||
}
|
|
||||||
func (mockStubs) SetDeprecated(context.Context, uint, bool) (model.Question, error) {
|
|
||||||
return model.Question{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReviewEndpoint(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
mockAuth := mockStubs{}
|
|
||||||
mockJobs := mockStubs{}
|
|
||||||
mockQuestions := mockStubs{}
|
|
||||||
mockParticipants := mockStubs{}
|
|
||||||
mockInvitations := mockInvitationService{}
|
|
||||||
|
|
||||||
h := handler.New(mockAuth, mockJobs, mockQuestions, mockParticipants, mockInvitations, 10, "http://localhost:5173")
|
|
||||||
tokens := security.NewTokenManager("01234567890123456789012345678901", time.Hour)
|
|
||||||
r := router.New(h, tokens, "http://localhost:5173")
|
|
||||||
|
|
||||||
// Create a valid token
|
|
||||||
token, err := tokens.Issue(1, "test@example.com", "interviewer")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create token: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the review endpoint
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/jobs/1/invitations/1", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
|
||||||
t.Errorf("Expected 200, got %d. Response: %s", w.Code, w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.Header().Get("Content-Type") != "application/json" {
|
|
||||||
t.Errorf("Expected JSON content type, got %s", w.Header().Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgres://go_guess:go_guess@db:5432/go_guess?sslmode=disable
|
DATABASE_URL: postgres://go_guess:go_guess@db:5432/go_guess?sslmode=disable
|
||||||
JWT_SECRET: local-development-secret-change-me-12345
|
JWT_SECRET: local-development-secret-change-me-12345
|
||||||
FRONTEND_URL: http://localhost:5173
|
FRONTEND_URL: ${FRONTEND_URL:-http://localhost:5173}
|
||||||
|
APP_ENV: ${APP_ENV:-development}
|
||||||
ports:
|
ports:
|
||||||
- "${API_PORT:-8080}:8080"
|
- "${API_PORT:-8080}:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue