From 6ffaafb4b53bead8b9a52a7f87f41dac4748f92d Mon Sep 17 00:00:00 2001 From: adityavshinde Date: Wed, 2 Sep 2026 15:22:28 +0530 Subject: [PATCH] Add Postgres e2e test suite and fix e2e client issues - Add DB layer e2e tests (error mapping, schema validation, lister behavior) under test/e2e/db/ with build tag e2e - Fix seedResults in lister_test.go: lowercase status names to match API name regex, populate mandatory Summary.Record and Summary.Type - Remove InsecureSkipVerify TLS fallback in test/e2e/client/config.go to resolve CodeQL critical alert; fail explicitly on missing certs - Add run_db_tests function in e2e.sh with port-forward and DB_URL setup; exclude db tests from main e2e run - Fix stale variable reference in e2e_gcs_test.go: allNamespacesReadAccessTokenFile to allNamespacesReadAccessToken --- test/e2e/README.md | 17 ++ test/e2e/client/config.go | 108 +++++++ test/e2e/db/README.md | 81 ++++++ test/e2e/db/db_test.go | 129 +++++++++ test/e2e/db/error_mapping_test.go | 247 ++++++++++++++++ test/e2e/db/labels_test.go | 33 +++ test/e2e/db/lister_test.go | 461 ++++++++++++++++++++++++++++++ test/e2e/db/migrations_test.go | 50 ++++ test/e2e/db/relationships_test.go | 32 +++ test/e2e/db/retention_test.go | 26 ++ test/e2e/db/schema_test.go | 244 ++++++++++++++++ test/e2e/e2e.sh | 49 +++- test/e2e/e2e_gcs_test.go | 2 +- test/e2e/e2e_test.go | 183 +++--------- 14 files changed, 1518 insertions(+), 144 deletions(-) create mode 100644 test/e2e/client/config.go create mode 100644 test/e2e/db/README.md create mode 100644 test/e2e/db/db_test.go create mode 100644 test/e2e/db/error_mapping_test.go create mode 100644 test/e2e/db/labels_test.go create mode 100644 test/e2e/db/lister_test.go create mode 100644 test/e2e/db/migrations_test.go create mode 100644 test/e2e/db/relationships_test.go create mode 100644 test/e2e/db/retention_test.go create mode 100644 test/e2e/db/schema_test.go diff --git a/test/e2e/README.md b/test/e2e/README.md index fddf73fcfb..99d9f4df86 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -74,6 +74,7 @@ Once you have configured your local client, you can run the tests by running: ```sh $ go test --tags=e2e . ``` + ### HA E2E Tests The E2E test suite verifies the three correctness guarantees using a real Kubernetes cluster with the HA configuration deployed. @@ -144,3 +145,19 @@ API pod tekton-results-api-def456: 12 requests API pod tekton-results-api-ghi789: 14 requests ``` +### Postgres DB layer tests + +The `db/` sub-package contains end-to-end tests that exercise +Postgres-specific behavior (error code mapping, schema validation, lister +filter/sort/pagination, jsonb). These run against the deployed API server and +its live Postgres instance — **not** an in-process server. + +See [`db/README.md`](db/README.md) for details on environment variables, +standalone runs, and how to add tests for future stories. + +```sh +$ go test -v -count=1 -tags=e2e ./test/e2e/db/... +``` + +The `e2e.sh` script runs these automatically after the main e2e suite; it +handles port-forwarding and credential wiring via `DB_URL`. diff --git a/test/e2e/client/config.go b/test/e2e/client/config.go new file mode 100644 index 0000000000..4457e9c31e --- /dev/null +++ b/test/e2e/client/config.go @@ -0,0 +1,108 @@ +package client + +import ( + "fmt" + "os" + "path" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "k8s.io/client-go/transport" +) + +// Default e2e environment values. These match the paths used by +// test/e2e/01-install.sh and the kind cluster setup scripts. +const ( + DefaultServerName = "tekton-results-api-service.tekton-pipelines.svc.cluster.local" + DefaultServerAddress = "https://localhost:8080" + DefaultCertFileName = "tekton-results-cert.pem" + DefaultCertPath = "/tmp/tekton-results/ssl" + DefaultTokenPath = "/tmp/tekton-results/tokens" //nolint:gosec // Not a credential; directory path for SA token files. + + AdminTokenFile = "all-namespaces-admin-access" + ReadTokenFile = "all-namespaces-read-access" +) + +// EnvConfig holds the resolved paths and addresses for the e2e test environment. +type EnvConfig struct { + CertFile string + TokenPath string + ServerName string + ServerAddress string +} + +// NewEnvConfig reads the standard e2e environment variables and falls back to +// defaults. Both the main e2e suite and the db sub-suite share this to avoid +// duplicating the env-var-to-default resolution logic. +func NewEnvConfig() EnvConfig { + certPath := EnvOrDefault("SSL_CERT_PATH", DefaultCertPath) + certFileName := EnvOrDefault("CERT_FILE_NAME", DefaultCertFileName) + return EnvConfig{ + CertFile: path.Join(certPath, certFileName), + TokenPath: EnvOrDefault("SA_TOKEN_PATH", DefaultTokenPath), + ServerName: EnvOrDefault("API_SERVER_NAME", DefaultServerName), + ServerAddress: EnvOrDefault("API_SERVER_ADDR", DefaultServerAddress), + } +} + +// TokenFile returns the full path for a given token file name under the +// resolved token directory. +func (c EnvConfig) TokenFile(name string) string { + return path.Join(c.TokenPath, name) +} + +// NewGRPCClientFromConfig creates a GRPCClient using the shared EnvConfig and +// the specified token file name. +func NewGRPCClientFromConfig(cfg EnvConfig, tokenFileName string, impersonationConfig *transport.ImpersonationConfig) (GRPCClient, error) { + if impersonationConfig == nil { + impersonationConfig = &transport.ImpersonationConfig{} + } + + transportCreds, err := credentials.NewClientTLSFromFile(cfg.CertFile, cfg.ServerName) + if err != nil { + return nil, fmt.Errorf("failed to load TLS credentials from %s: %w", cfg.CertFile, err) + } + + opts := []grpc.DialOption{ + grpc.WithBlock(), //nolint:staticcheck + grpc.WithTransportCredentials(transportCreds), + grpc.WithDefaultCallOptions(grpc.PerRPCCredentials(&CustomCredentials{ + TokenSource: transport.NewCachedFileTokenSource(cfg.TokenFile(tokenFileName)), + ImpersonationConfig: impersonationConfig, + })), + } + + return NewGRPCClient(cfg.ServerAddress, opts...) +} + +// NewRESTClientFromConfig creates a RESTClient using the shared EnvConfig and +// the specified token file name. +func NewRESTClientFromConfig(cfg EnvConfig, tokenFileName string, impersonationConfig *transport.ImpersonationConfig) (RESTClient, error) { + if impersonationConfig == nil { + impersonationConfig = &transport.ImpersonationConfig{} + } + + if _, err := credentials.NewClientTLSFromFile(cfg.CertFile, cfg.ServerName); err != nil { + return nil, fmt.Errorf("failed to verify TLS cert from %s: %w", cfg.CertFile, err) + } + + restConfig := &transport.Config{ + TLS: transport.TLSConfig{ + CAFile: cfg.CertFile, + ServerName: cfg.ServerName, + }, + BearerTokenFile: cfg.TokenFile(tokenFileName), + Impersonate: *impersonationConfig, + } + + return NewRESTClient(cfg.ServerAddress, WithConfig(restConfig)) +} + +// EnvOrDefault returns the value of the environment variable named by key, +// or fallback if the variable is empty or unset. +func EnvOrDefault(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/test/e2e/db/README.md b/test/e2e/db/README.md new file mode 100644 index 0000000000..2bd10cc052 --- /dev/null +++ b/test/e2e/db/README.md @@ -0,0 +1,81 @@ +# Postgres DB Layer E2E Tests + +End-to-end tests that validate the Postgres-specific database layer of Tekton +Results. These tests run against the **deployed** API server in the kind e2e +cluster and its **live** Postgres instance—not an in-process server or a +throwaway database. + +## What these tests cover + +| Area | File | Status | +|---|---|---| +| Error mapping (pgconn.PgError → gRPC codes) | `error_mapping_test.go` | Active | +| Schema validation (column types, PKs, indexes, FKs, jsonb) | `schema_test.go` | Active | +| Lister behavior (filter, sort, pagination via Postgres) | `lister_test.go` | Active | +| Labels (normalized table, selector operators) | `labels_test.go` | Placeholder (Stories 11-13) | +| Metadata columns (text[], GIN indexes) | `migrations_test.go` | Placeholder (Stories 08-10) | +| golang-migrate migrations | `migrations_test.go` | Placeholder (Story 07) | +| Relationships & retention | `relationships_test.go`, `retention_test.go` | Placeholder (Stories 17, 20, 21) | + +## Prerequisites + +- A running kind cluster with Tekton Results installed (`./test/e2e/00-setup.sh` + and `./test/e2e/01-install.sh`). +- The API server must be accessible at `localhost:8080` (the kind nodePort). +- Postgres must be reachable via `kubectl port-forward` (the `e2e.sh` script + handles this automatically). + +## Running + +### Via the e2e script (CI path) + +```bash +./test/e2e/e2e.sh +``` + +The script runs the standard e2e tests, then the DB layer tests, then the GCS +logging tests. + +### Standalone (after cluster is up) + +```bash +# Start port-forward to Postgres +kubectl port-forward svc/tekton-results-postgres-service 15432:5432 -n tekton-pipelines & + +# Read the password +PGPASS=$(kubectl get secret tekton-results-postgres -n tekton-pipelines \ + -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d) + +# Run (single DB_URL) +DB_URL="host=localhost port=15432 user=postgres password=${PGPASS} dbname=tekton-results sslmode=disable" \ +go test -v -count=1 -tags=e2e ./test/e2e/db/... +``` + +## Environment variables + +| Variable | Default | Description | +|---|---|---| +| `DB_URL` | (none) | Full Postgres DSN. When set, takes precedence over individual `POSTGRES_*` vars. The `e2e.sh` script constructs this automatically. | +| `POSTGRES_HOST` | `localhost` | Fallback: Postgres host (via port-forward) | +| `POSTGRES_PORT` | `15432` | Fallback: local forwarded port | +| `POSTGRES_USER` | `postgres` | Fallback: DB user | +| `POSTGRES_PASSWORD` | (none) | Fallback: DB password (from cluster secret) | +| `POSTGRES_DB` | `tekton-results` | Fallback: database name (the live API database) | +| `POSTGRES_SSLMODE` | `disable` | Fallback: SSL mode | +| `SSL_CERT_PATH` | `/tmp/tekton-results/ssl` | Path to TLS cert for API | +| `SA_TOKEN_PATH` | `/tmp/tekton-results/tokens` | Path to SA token files | +| `API_SERVER_ADDR` | `https://localhost:8080` | API server address | +| `API_SERVER_NAME` | `tekton-results-api-service.tekton-pipelines.svc.cluster.local` | TLS server name | + +## Adding tests for future stories + +When your story lands a Postgres-specific feature: + +1. Find the matching placeholder file (e.g., `labels_test.go`). +2. Replace the `t.Skip(...)` body with real test logic. +3. If no placeholder exists, create a new `*_test.go` file with the + `//go:build e2e` tag. +4. Use `adminClient` for mutations, `readClient` for queries, and `rawDB` + for direct schema introspection. +5. Clean up test data via the API in `t.Cleanup`—never drop or truncate + tables (it is the live database). diff --git a/test/e2e/db/db_test.go b/test/e2e/db/db_test.go new file mode 100644 index 0000000000..79628f47e5 --- /dev/null +++ b/test/e2e/db/db_test.go @@ -0,0 +1,129 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package db_test provides end-to-end tests for the Postgres database layer. +// +// Tests exercise the deployed API server (gRPC) in the kind cluster against +// its live Postgres instance, verifying behavior that the SQLite-backed unit +// tests cannot reach: +// - Postgres-specific error code mapping (pgconn.PgError → gRPC status) +// - Lister filter/sort/pagination with real Postgres query execution +// - Schema correctness (column types, indexes, foreign keys, jsonb) +// +// A raw database connection is used only for schema introspection and to +// trigger FK violations that the server's pre-checks would otherwise mask. +// +// Future stories (labels, metadata columns, golang-migrate migrations) add +// their Postgres-specific tests to this package. See the stub files for +// placeholders. +// +// Run: +// +// go test -v -count=1 -tags=e2e ./test/e2e/db/... +package db_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/tektoncd/results/test/e2e/client" + "gorm.io/driver/postgres" + "gorm.io/gorm" + + // Register the Postgres error space so pgconn.PgError → gRPC code + // translation is active in the dberrors.Wrap calls used by the FK + // violation test. + _ "github.com/tektoncd/results/pkg/api/server/db/errors/postgres" +) + +var ( + // adminClient talks to the deployed API server with full CRUD + // permissions. Used for creating/updating/deleting test data. + adminClient client.GRPCClient + + // readClient talks to the deployed API server with read-only + // permissions. Used for list/get assertions where we want to confirm + // the read path independently. + readClient client.GRPCClient + + // rawDB is a direct Postgres connection to the live tekton-results + // database. Used only for schema introspection (information_schema / + // pg_catalog) and for the FK violation test that must bypass the + // server's pre-check logic. + rawDB *gorm.DB +) + +func TestMain(m *testing.M) { + cfg := client.NewEnvConfig() + var err error + + adminClient, err = client.NewGRPCClientFromConfig(cfg, client.AdminTokenFile, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create admin gRPC client: %v\n", err) + os.Exit(1) + } + + readClient, err = client.NewGRPCClientFromConfig(cfg, client.ReadTokenFile, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create read gRPC client: %v\n", err) + os.Exit(1) + } + + dsn := buildDSN() + rawDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to connect to postgres: %v\n", err) + os.Exit(1) + } + sqlDB, err := rawDB.DB() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to get underlying sql.DB: %v\n", err) + os.Exit(1) + } + if err := sqlDB.Ping(); err != nil { + fmt.Fprintf(os.Stderr, "failed to ping postgres: %v\n", err) + os.Exit(1) + } + + code := m.Run() + + sqlDB.Close() + os.Exit(code) +} + +func buildDSN() string { + if url := os.Getenv("DB_URL"); url != "" { + return url + } + host := client.EnvOrDefault("POSTGRES_HOST", "localhost") + port := client.EnvOrDefault("POSTGRES_PORT", "15432") + user := client.EnvOrDefault("POSTGRES_USER", "postgres") + pass := client.EnvOrDefault("POSTGRES_PASSWORD", "") + dbname := client.EnvOrDefault("POSTGRES_DB", "tekton-results") + sslmode := client.EnvOrDefault("POSTGRES_SSLMODE", "disable") + + return fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, port, user, pass, dbname, sslmode, + ) +} + +func defaultCtx() context.Context { + return context.Background() +} diff --git a/test/e2e/db/error_mapping_test.go b/test/e2e/db/error_mapping_test.go new file mode 100644 index 0000000000..a0d2035995 --- /dev/null +++ b/test/e2e/db/error_mapping_test.go @@ -0,0 +1,247 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + model "github.com/tektoncd/results/pkg/api/server/db" + dberrors "github.com/tektoncd/results/pkg/api/server/db/errors" + pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// TestErrorMapping_UniqueViolationResult verifies that creating a Result +// with a duplicate name through the deployed API returns codes.AlreadyExists. +// +// Path exercised: gRPC request → API server → GORM insert → Postgres returns +// pgconn.PgError SQLSTATE 23505 → errors.Wrap → translate() → AlreadyExists. +// SQLite unit tests can never reach translate() because SQLite returns a +// different error type that does not implement the SQLState() interface. +func TestErrorMapping_UniqueViolationResult(t *testing.T) { + ctx := context.Background() + const parent = "db-err-dup-res" + + req := &pb.CreateResultRequest{ + Parent: parent, + Result: &pb.Result{ + Name: parent + "/results/dup-result", + }, + } + + res, err := adminClient.CreateResult(ctx, req) + if err != nil { + t.Fatalf("first CreateResult failed: %v", err) + } + t.Cleanup(func() { deleteResult(t, res.GetName()) }) + + _, err = adminClient.CreateResult(ctx, req) + if err == nil { + t.Fatal("expected error on duplicate Result, got nil") + } + if got := status.Code(err); got != codes.AlreadyExists { + t.Errorf("want gRPC code %s, got %s (err: %v)", codes.AlreadyExists, got, err) + } +} + +// TestErrorMapping_UniqueViolationRecord verifies duplicate Record detection +// through the deployed API against real Postgres. +func TestErrorMapping_UniqueViolationRecord(t *testing.T) { + ctx := context.Background() + const parent = "db-err-dup-rec" + + res, err := adminClient.CreateResult(ctx, &pb.CreateResultRequest{ + Parent: parent, + Result: &pb.Result{Name: parent + "/results/parent-for-dup-rec"}, + }) + if err != nil { + t.Fatalf("CreateResult failed: %v", err) + } + t.Cleanup(func() { deleteResult(t, res.GetName()) }) + + recReq := &pb.CreateRecordRequest{ + Parent: res.GetName(), + Record: &pb.Record{ + Name: res.GetName() + "/records/dup-record", + Data: &pb.Any{ + Type: "testing.tekton.dev/test", + Value: mustJSON(t, map[string]string{"key": "value"}), + }, + }, + } + + if _, err := adminClient.CreateRecord(ctx, recReq); err != nil { + t.Fatalf("first CreateRecord failed: %v", err) + } + + _, err = adminClient.CreateRecord(ctx, recReq) + if err == nil { + t.Fatal("expected error on duplicate Record, got nil") + } + if got := status.Code(err); got != codes.AlreadyExists { + t.Errorf("want gRPC code %s, got %s (err: %v)", codes.AlreadyExists, got, err) + } +} + +// TestErrorMapping_ForeignKeyViolation verifies that Postgres SQLSTATE 23503 +// (foreign_key_violation) is translated to codes.FailedPrecondition. +// +// The server's CreateRecord pre-checks the parent via getResultID, returning +// NotFound before the INSERT ever runs. To hit the actual FK constraint we +// must bypass the server and INSERT directly into the records table via the +// raw DB connection, then wrap the error through dberrors.Wrap. +func TestErrorMapping_ForeignKeyViolation(t *testing.T) { + orphan := &model.Record{ + Parent: "db-err-fk", + ResultID: "nonexistent-result-id", + ResultName: "nonexistent-result", + ID: "orphan-record-id", + Name: "orphan-record", + Type: "testing.tekton.dev/test", + Data: []byte(`{"orphan":true}`), + Etag: "test", + CreatedTime: time.Now(), + UpdatedTime: time.Now(), + } + + t.Cleanup(func() { rawDB.Delete(orphan) }) + + err := rawDB.Create(orphan).Error + if err == nil { + t.Fatal("expected FK violation error from direct insert, got nil") + } + + wrapped := dberrors.Wrap(err) + if got := status.Code(wrapped); got != codes.FailedPrecondition { + t.Errorf("want gRPC code %s for FK violation, got %s (raw err: %v)", + codes.FailedPrecondition, got, err) + } +} + +// TestErrorMapping_NotFound verifies that fetching a non-existent Result +// through the deployed API returns codes.NotFound. +func TestErrorMapping_NotFound(t *testing.T) { + ctx := context.Background() + + _, err := readClient.GetResult(ctx, &pb.GetResultRequest{ + Name: "db-err-notfound/results/does-not-exist", + }) + if err == nil { + t.Fatal("expected NotFound error, got nil") + } + if got := status.Code(err); got != codes.NotFound { + t.Errorf("want gRPC code %s, got %s (err: %v)", codes.NotFound, got, err) + } +} + +// TestErrorMapping_StaleEtag verifies that updating a Result with a stale +// etag through the deployed API returns codes.FailedPrecondition. +func TestErrorMapping_StaleEtag(t *testing.T) { + ctx := context.Background() + const parent = "db-err-etag" + + res, err := adminClient.CreateResult(ctx, &pb.CreateResultRequest{ + Parent: parent, + Result: &pb.Result{ + Name: parent + "/results/etag-test", + Annotations: map[string]string{"v": "1"}, + }, + }) + if err != nil { + t.Fatalf("CreateResult failed: %v", err) + } + t.Cleanup(func() { deleteResult(t, res.GetName()) }) + + _, err = adminClient.UpdateResult(ctx, &pb.UpdateResultRequest{ + Name: res.GetName(), + Result: &pb.Result{ + Name: res.GetName(), + Annotations: map[string]string{"v": "2"}, + }, + Etag: "deliberately-wrong-etag", + }) + if err == nil { + t.Fatal("expected FailedPrecondition for stale etag, got nil") + } + if got := status.Code(err); got != codes.FailedPrecondition { + t.Errorf("want gRPC code %s, got %s (err: %v)", codes.FailedPrecondition, got, err) + } +} + +// TestErrorMapping_CascadeDelete verifies ON DELETE CASCADE through the +// deployed API: deleting a Result must also delete its child Records. +func TestErrorMapping_CascadeDelete(t *testing.T) { + ctx := context.Background() + const parent = "db-err-cascade" + + res, err := adminClient.CreateResult(ctx, &pb.CreateResultRequest{ + Parent: parent, + Result: &pb.Result{Name: parent + "/results/cascade-parent"}, + }) + if err != nil { + t.Fatalf("CreateResult failed: %v", err) + } + + rec, err := adminClient.CreateRecord(ctx, &pb.CreateRecordRequest{ + Parent: res.GetName(), + Record: &pb.Record{ + Name: res.GetName() + "/records/cascade-child", + Data: &pb.Any{ + Type: "testing.tekton.dev/test", + Value: mustJSON(t, map[string]string{"cascade": "true"}), + }, + }, + }) + if err != nil { + t.Fatalf("CreateRecord failed: %v", err) + } + + if _, err := adminClient.DeleteResult(ctx, &pb.DeleteResultRequest{ + Name: res.GetName(), + }); err != nil { + t.Fatalf("DeleteResult failed: %v", err) + } + + _, err = readClient.GetRecord(ctx, &pb.GetRecordRequest{Name: rec.GetName()}) + if err == nil { + t.Fatal("expected Record to be deleted via cascade, but GetRecord succeeded") + } + if got := status.Code(err); got != codes.NotFound { + t.Errorf("want gRPC code %s after cascade delete, got %s (err: %v)", + codes.NotFound, got, err) + } +} + +// deleteResult is a best-effort cleanup helper. +func deleteResult(t *testing.T, name string) { + t.Helper() + _, _ = adminClient.DeleteResult(context.Background(), &pb.DeleteResultRequest{Name: name}) +} + +func mustJSON(t *testing.T, v any) []byte { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + return b +} diff --git a/test/e2e/db/labels_test.go b/test/e2e/db/labels_test.go new file mode 100644 index 0000000000..29da2ced9c --- /dev/null +++ b/test/e2e/db/labels_test.go @@ -0,0 +1,33 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import "testing" + +// TestLabels_NormalizedTable will verify the normalized label table, dedup +// logic, and the six selector operators once Stories 11-13 land. +// See: SRVKP Stories 11, 12, 13. +func TestLabels_NormalizedTable(t *testing.T) { + t.Skip("labels table not yet implemented (Stories 11-13)") +} + +// TestLabels_SelectorOperators will verify label selector operators +// (=, !=, in, notin, exists, !exists) against real Postgres. +func TestLabels_SelectorOperators(t *testing.T) { + t.Skip("labels table not yet implemented (Stories 11-13)") +} diff --git a/test/e2e/db/lister_test.go b/test/e2e/db/lister_test.go new file mode 100644 index 0000000000..68e4d15e17 --- /dev/null +++ b/test/e2e/db/lister_test.go @@ -0,0 +1,461 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// seedResults creates n Results via the deployed API under the given parent. +// Returns the created Result protos in creation order. +// +// The API requires Summary.Record and Summary.Type whenever Summary is non-nil, +// so we populate them with well-formed placeholders. Result and record names +// must be lowercase per the API's name regex. +func seedResults(t *testing.T, parent string, n int, summaryStatus pb.RecordSummary_Status) []*pb.Result { + t.Helper() + ctx := context.Background() + results := make([]*pb.Result, 0, n) + + statusName := strings.ToLower(pb.RecordSummary_Status_name[int32(summaryStatus)]) + for i := 0; i < n; i++ { + resultID := fmt.Sprintf("lister-%s-%d", statusName, i) + name := fmt.Sprintf("%s/results/%s", parent, resultID) + recordName := fmt.Sprintf("%s/records/summary", name) + res, err := adminClient.CreateResult(ctx, &pb.CreateResultRequest{ + Parent: parent, + Result: &pb.Result{ + Name: name, + Annotations: map[string]string{ + "index": fmt.Sprintf("%d", i), + "series": statusName, + }, + Summary: &pb.RecordSummary{ + Record: recordName, + Type: "testing.tekton.dev/test", + Status: summaryStatus, + }, + }, + }) + if err != nil { + t.Fatalf("seedResults: CreateResult(%s) failed: %v", name, err) + } + results = append(results, res) + } + return results +} + +// cleanupParent deletes all Results under the given parent via the API. +func cleanupParent(t *testing.T, parent string) { + t.Helper() + ctx := context.Background() + resp, err := adminClient.ListResults(ctx, &pb.ListResultsRequest{Parent: parent}) + if err != nil { + return + } + for _, r := range resp.Results { + _, _ = adminClient.DeleteResult(ctx, &pb.DeleteResultRequest{Name: r.GetName()}) + } +} + +// TestLister_BasicList verifies ListResults returns all Results under a parent. +func TestLister_BasicList(t *testing.T) { + const parent = "db-list-basic" + t.Cleanup(func() { cleanupParent(t, parent) }) + seeded := seedResults(t, parent, 6, pb.RecordSummary_SUCCESS) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + }) + if err != nil { + t.Fatalf("ListResults failed: %v", err) + } + + if got := len(resp.Results); got != len(seeded) { + t.Errorf("want %d results, got %d", len(seeded), got) + } + + // Verify every seeded name appears in the response. + gotNames := make(map[string]bool, len(resp.Results)) + for _, r := range resp.Results { + gotNames[r.Name] = true + } + for _, s := range seeded { + if !gotNames[s.Name] { + t.Errorf("seeded Result %q not in ListResults response", s.Name) + } + } +} + +// TestLister_WildcardParent verifies that parent="-" returns Results across +// all namespaces. +func TestLister_WildcardParent(t *testing.T) { + const parentA = "db-list-wc-a" + const parentB = "db-list-wc-b" + t.Cleanup(func() { + cleanupParent(t, parentA) + cleanupParent(t, parentB) + }) + + seededA := seedResults(t, parentA, 3, pb.RecordSummary_SUCCESS) + seededB := seedResults(t, parentB, 3, pb.RecordSummary_FAILURE) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: "-", + }) + if err != nil { + t.Fatalf("ListResults with wildcard parent failed: %v", err) + } + + gotNames := make(map[string]bool, len(resp.Results)) + for _, r := range resp.Results { + gotNames[r.Name] = true + } + + for _, s := range seededA { + if !gotNames[s.Name] { + t.Errorf("seeded Result %q from parent %s not found with wildcard parent", s.Name, parentA) + } + } + for _, s := range seededB { + if !gotNames[s.Name] { + t.Errorf("seeded Result %q from parent %s not found with wildcard parent", s.Name, parentB) + } + } +} + +// TestLister_FilterByStatus verifies the CEL→SQL filter summary.status == SUCCESS +// executes correctly on Postgres. +func TestLister_FilterByStatus(t *testing.T) { + const parent = "db-list-filter" + t.Cleanup(func() { cleanupParent(t, parent) }) + + seedResults(t, parent, 5, pb.RecordSummary_SUCCESS) + seedResults(t, parent, 4, pb.RecordSummary_FAILURE) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + Filter: "summary.status == SUCCESS", + }) + if err != nil { + t.Fatalf("ListResults with filter failed: %v", err) + } + + if got := len(resp.Results); got != 5 { + t.Errorf("want 5 SUCCESS results, got %d", got) + } + for _, r := range resp.Results { + if r.Summary.Status != pb.RecordSummary_SUCCESS { + t.Errorf("result %s leaked non-SUCCESS status %v", r.Name, r.Summary.Status) + } + } +} + +// TestLister_FilterNotEqual verifies the != CEL operator. +func TestLister_FilterNotEqual(t *testing.T) { + const parent = "db-list-neq" + t.Cleanup(func() { cleanupParent(t, parent) }) + + seedResults(t, parent, 3, pb.RecordSummary_SUCCESS) + seedResults(t, parent, 4, pb.RecordSummary_FAILURE) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + Filter: "summary.status != SUCCESS", + }) + if err != nil { + t.Fatalf("ListResults with != filter failed: %v", err) + } + + if got := len(resp.Results); got != 4 { + t.Errorf("want 4 non-SUCCESS results, got %d", got) + } +} + +// TestLister_OrderByCreateTimeDesc verifies descending ORDER BY on Postgres. +func TestLister_OrderByCreateTimeDesc(t *testing.T) { + const parent = "db-list-order-desc" + t.Cleanup(func() { cleanupParent(t, parent) }) + seedResults(t, parent, 6, pb.RecordSummary_SUCCESS) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + OrderBy: "create_time desc", + }) + if err != nil { + t.Fatalf("ListResults with ORDER BY failed: %v", err) + } + + for i := 1; i < len(resp.Results); i++ { + prev := resp.Results[i-1].CreateTime.AsTime() + curr := resp.Results[i].CreateTime.AsTime() + if prev.Before(curr) { + t.Errorf("result[%d] create_time (%v) < result[%d] (%v); want DESC", + i-1, prev, i, curr) + } + } +} + +// TestLister_OrderByCreateTimeAsc verifies ascending ORDER BY on Postgres. +func TestLister_OrderByCreateTimeAsc(t *testing.T) { + const parent = "db-list-order-asc" + t.Cleanup(func() { cleanupParent(t, parent) }) + seedResults(t, parent, 6, pb.RecordSummary_SUCCESS) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + OrderBy: "create_time asc", + }) + if err != nil { + t.Fatalf("ListResults with ORDER BY ASC failed: %v", err) + } + + for i := 1; i < len(resp.Results); i++ { + prev := resp.Results[i-1].CreateTime.AsTime() + curr := resp.Results[i].CreateTime.AsTime() + if prev.After(curr) { + t.Errorf("result[%d] create_time (%v) > result[%d] (%v); want ASC", + i-1, prev, i, curr) + } + } +} + +// TestLister_Pagination verifies keyset pagination on Postgres. Uses +// pageSize=5 (the minimum allowed by the lister). Checks per-page size, +// total count, and no duplicates. +func TestLister_Pagination(t *testing.T) { + const parent = "db-list-page" + const total = 13 + const pageSize int32 = 5 + t.Cleanup(func() { cleanupParent(t, parent) }) + seedResults(t, parent, total, pb.RecordSummary_SUCCESS) + + var allNames []string + pageToken := "" + + for page := 0; ; page++ { + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + PageSize: pageSize, + PageToken: pageToken, + }) + if err != nil { + t.Fatalf("page %d: ListResults failed: %v", page, err) + } + + if resp.NextPageToken != "" && int32(len(resp.Results)) != pageSize { + t.Errorf("page %d: non-final page has %d items, want %d", + page, len(resp.Results), pageSize) + } + + for _, r := range resp.Results { + allNames = append(allNames, r.Name) + } + + if resp.NextPageToken == "" { + break + } + pageToken = resp.NextPageToken + + if page > 20 { + t.Fatal("pagination did not terminate after 20 pages") + } + } + + if got := len(allNames); got != total { + t.Errorf("pagination returned %d results, want %d", got, total) + } + + seen := make(map[string]bool) + for _, name := range allNames { + if seen[name] { + t.Errorf("duplicate result in pagination: %s", name) + } + seen[name] = true + } +} + +// TestLister_PaginationWithFilterAndOrder exercises the full query pipeline +// (filter + order + pagination) on Postgres. +// It verifies status filtering, descending create_time order across pages, +// no duplicate names, and the expected total count. +func TestLister_PaginationWithFilterAndOrder(t *testing.T) { + const parent = "db-list-combo" + t.Cleanup(func() { cleanupParent(t, parent) }) + seedResults(t, parent, 12, pb.RecordSummary_SUCCESS) + seedResults(t, parent, 6, pb.RecordSummary_FAILURE) + + seenNames := make(map[string]bool) + var prevCreateTime time.Time + var totalCount int + pageToken := "" + + for page := 0; ; page++ { + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + Filter: "summary.status == SUCCESS", + OrderBy: "create_time desc", + PageSize: 5, + PageToken: pageToken, + }) + if err != nil { + t.Fatalf("page %d: ListResults failed: %v", page, err) + } + + for _, r := range resp.Results { + if r.Summary.Status != pb.RecordSummary_SUCCESS { + t.Errorf("filter leaked non-SUCCESS result: %s (status=%v)", r.Name, r.Summary.Status) + } + + if seenNames[r.Name] { + t.Errorf("duplicate result across pages: %s", r.Name) + } + seenNames[r.Name] = true + + ct := r.CreateTime.AsTime() + if totalCount > 0 && ct.After(prevCreateTime) { + t.Errorf("result %s create_time (%v) is after previous (%v); want DESC order across pages", + r.Name, ct, prevCreateTime) + } + prevCreateTime = ct + totalCount++ + } + + if resp.NextPageToken == "" { + break + } + pageToken = resp.NextPageToken + + if page > 20 { + t.Fatal("pagination did not terminate") + } + } + + if totalCount != 12 { + t.Errorf("want 12 SUCCESS results, got %d", totalCount) + } +} + +// TestLister_EmptyResult verifies that a filter matching nothing returns an +// empty list with no page token. +func TestLister_EmptyResult(t *testing.T) { + const parent = "db-list-empty" + t.Cleanup(func() { cleanupParent(t, parent) }) + seedResults(t, parent, 5, pb.RecordSummary_SUCCESS) + + resp, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: parent, + Filter: "summary.status == FAILURE", + }) + if err != nil { + t.Fatalf("ListResults failed: %v", err) + } + if got := len(resp.Results); got != 0 { + t.Errorf("want 0 results, got %d", got) + } + if resp.NextPageToken != "" { + t.Errorf("want empty NextPageToken, got %q", resp.NextPageToken) + } +} + +// TestLister_ListRecords verifies ListRecords against the deployed Postgres. +func TestLister_ListRecords(t *testing.T) { + const parent = "db-list-records" + t.Cleanup(func() { cleanupParent(t, parent) }) + + res, err := adminClient.CreateResult(defaultCtx(), &pb.CreateResultRequest{ + Parent: parent, + Result: &pb.Result{Name: parent + "/results/with-records"}, + }) + if err != nil { + t.Fatalf("CreateResult failed: %v", err) + } + + const numRecords = 7 + createdNames := make([]string, 0, numRecords) + for i := 0; i < numRecords; i++ { + recName := fmt.Sprintf("%s/records/rec-%d", res.GetName(), i) + rec, err := adminClient.CreateRecord(defaultCtx(), &pb.CreateRecordRequest{ + Parent: res.GetName(), + Record: &pb.Record{ + Name: recName, + Data: &pb.Any{ + Type: "testing.tekton.dev/test", + Value: mustJSON(t, map[string]string{"i": fmt.Sprintf("%d", i)}), + }, + }, + }) + if err != nil { + t.Fatalf("CreateRecord(%s) failed: %v", recName, err) + } + createdNames = append(createdNames, rec.GetName()) + } + + resp, err := readClient.ListRecords(defaultCtx(), &pb.ListRecordsRequest{ + Parent: res.GetName(), + }) + if err != nil { + t.Fatalf("ListRecords failed: %v", err) + } + + gotNames := make(map[string]bool, len(resp.Records)) + for _, r := range resp.Records { + gotNames[r.GetName()] = true + } + for _, name := range createdNames { + if !gotNames[name] { + t.Errorf("created Record %q not in ListRecords response", name) + } + } +} + +// TestLister_InvalidFilter verifies invalid CEL returns InvalidArgument. +func TestLister_InvalidFilter(t *testing.T) { + _, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: "db-list-invalid", + Filter: "this is not a valid CEL expression !!!", + }) + if err == nil { + t.Fatal("expected error for invalid filter, got nil") + } + if got := status.Code(err); got != codes.InvalidArgument { + t.Errorf("want gRPC code %s, got %s (err: %v)", codes.InvalidArgument, got, err) + } +} + +// TestLister_InvalidOrderBy verifies unsupported order_by returns InvalidArgument. +func TestLister_InvalidOrderBy(t *testing.T) { + _, err := readClient.ListResults(defaultCtx(), &pb.ListResultsRequest{ + Parent: "db-list-invalid-order", + OrderBy: "nonexistent_field", + }) + if err == nil { + t.Fatal("expected error for invalid order_by, got nil") + } + if got := status.Code(err); got != codes.InvalidArgument { + t.Errorf("want gRPC code %s, got %s (err: %v)", codes.InvalidArgument, got, err) + } +} diff --git a/test/e2e/db/migrations_test.go b/test/e2e/db/migrations_test.go new file mode 100644 index 0000000000..f04e0ced1e --- /dev/null +++ b/test/e2e/db/migrations_test.go @@ -0,0 +1,50 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import "testing" + +// TestMigrations_GolangMigrateApply will verify that versioned .sql migration +// files apply cleanly to a fresh Postgres database once Story 07 lands. +func TestMigrations_GolangMigrateApply(t *testing.T) { + t.Skip("golang-migrate not yet implemented (Story 07)") +} + +// TestMigrations_BaselineThenMigrate will verify that a GORM AutoMigrate +// database can be baselined and then receive golang-migrate migrations. +func TestMigrations_BaselineThenMigrate(t *testing.T) { + t.Skip("golang-migrate not yet implemented (Story 07)") +} + +// TestMigrations_Idempotent will verify that running the full migration +// sequence twice is a no-op. +func TestMigrations_Idempotent(t *testing.T) { + t.Skip("golang-migrate not yet implemented (Story 07)") +} + +// TestMigrations_VersionGate will verify that the server rejects databases +// at too-old or too-new migration versions. +func TestMigrations_VersionGate(t *testing.T) { + t.Skip("golang-migrate not yet implemented (Story 07)") +} + +// TestMigrations_MetadataColumns will verify the text[] metadata columns +// and GIN indexes once Stories 08-10 land. +func TestMigrations_MetadataColumns(t *testing.T) { + t.Skip("metadata columns not yet implemented (Stories 08-10)") +} diff --git a/test/e2e/db/relationships_test.go b/test/e2e/db/relationships_test.go new file mode 100644 index 0000000000..7ef57bebc6 --- /dev/null +++ b/test/e2e/db/relationships_test.go @@ -0,0 +1,32 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import "testing" + +// TestRelationships_StickyGrouping will verify sticky grouping via +// owner_uid columns and GIN indexes once Stories 17, 20, 21 land. +func TestRelationships_StickyGrouping(t *testing.T) { + t.Skip("relationship tables not yet implemented (Stories 17, 20, 21)") +} + +// TestRelationships_CRDCleanup will verify that CRD cleanup respects +// relationship constraints. +func TestRelationships_CRDCleanup(t *testing.T) { + t.Skip("relationship tables not yet implemented (Stories 17, 20, 21)") +} diff --git a/test/e2e/db/retention_test.go b/test/e2e/db/retention_test.go new file mode 100644 index 0000000000..fd05095581 --- /dev/null +++ b/test/e2e/db/retention_test.go @@ -0,0 +1,26 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import "testing" + +// TestRetention_PolicyEnforcement will verify that retention policies +// correctly delete aged records from Postgres once the retention story lands. +func TestRetention_PolicyEnforcement(t *testing.T) { + t.Skip("retention policy not yet implemented (Story 21)") +} diff --git a/test/e2e/db/schema_test.go b/test/e2e/db/schema_test.go new file mode 100644 index 0000000000..3787c5c503 --- /dev/null +++ b/test/e2e/db/schema_test.go @@ -0,0 +1,244 @@ +//go:build e2e +// +build e2e + +// Copyright 2026 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db_test + +import ( + "testing" + + pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" +) + +// TestSchema_TablesExist verifies that the deployed API server's AutoMigrate +// created both core tables in the live Postgres instance. +func TestSchema_TablesExist(t *testing.T) { + for _, table := range []string{"results", "records"} { + t.Run(table, func(t *testing.T) { + var exists bool + err := rawDB.Raw( + `SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = ? + )`, table, + ).Scan(&exists).Error + + if err != nil { + t.Fatalf("query failed: %v", err) + } + if !exists { + t.Errorf("expected table %q to exist in public schema", table) + } + }) + } +} + +// TestSchema_ColumnTypes verifies that GORM tags produce the correct Postgres +// column types on the live database. SQLite silently ignores type:jsonb +// and stores it as TEXT; Postgres must have actual jsonb. +func TestSchema_ColumnTypes(t *testing.T) { + type columnSpec struct { + table string + column string + wantType string + } + + specs := []columnSpec{ + {table: "results", column: "parent", wantType: "character varying"}, + {table: "results", column: "id", wantType: "character varying"}, + {table: "results", column: "name", wantType: "character varying"}, + {table: "results", column: "annotations", wantType: "jsonb"}, + {table: "results", column: "etag", wantType: "character varying"}, + {table: "results", column: "created_time", wantType: "timestamp with time zone"}, + {table: "results", column: "updated_time", wantType: "timestamp with time zone"}, + + {table: "records", column: "parent", wantType: "character varying"}, + {table: "records", column: "result_id", wantType: "character varying"}, + {table: "records", column: "id", wantType: "character varying"}, + {table: "records", column: "name", wantType: "character varying"}, + {table: "records", column: "data", wantType: "jsonb"}, + {table: "records", column: "type", wantType: "character varying"}, + {table: "records", column: "etag", wantType: "character varying"}, + } + + for _, spec := range specs { + t.Run(spec.table+"/"+spec.column, func(t *testing.T) { + var dataType string + err := rawDB.Raw( + `SELECT data_type FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = ? AND column_name = ?`, + spec.table, spec.column, + ).Scan(&dataType).Error + + if err != nil { + t.Fatalf("query failed: %v", err) + } + if dataType == "" { + t.Fatalf("column %s.%s not found in public schema", spec.table, spec.column) + } + if dataType != spec.wantType { + t.Errorf("column %s.%s: want type %q, got %q", + spec.table, spec.column, spec.wantType, dataType) + } + }) + } +} + +// TestSchema_PrimaryKeys verifies composite primary keys on both tables. +func TestSchema_PrimaryKeys(t *testing.T) { + type pkSpec struct { + table string + wantColumns []string + } + + specs := []pkSpec{ + {table: "results", wantColumns: []string{"id", "parent"}}, + {table: "records", wantColumns: []string{"id", "parent", "result_id"}}, + } + + for _, spec := range specs { + t.Run(spec.table, func(t *testing.T) { + var columns []string + err := rawDB.Raw(` + SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid + AND a.attnum = ANY(i.indkey) + JOIN pg_class c ON c.oid = i.indrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' + AND c.relname = ? + AND i.indisprimary + ORDER BY a.attname`, + spec.table, + ).Scan(&columns).Error + + if err != nil { + t.Fatalf("query failed: %v", err) + } + if len(columns) != len(spec.wantColumns) { + t.Fatalf("want PK columns %v, got %v", spec.wantColumns, columns) + } + for i, want := range spec.wantColumns { + if columns[i] != want { + t.Errorf("PK column %d: want %q, got %q", i, want, columns[i]) + } + } + }) + } +} + +// TestSchema_UniqueIndexes verifies unique indexes defined in GORM model tags +// exist and are actually unique in the live Postgres schema. +func TestSchema_UniqueIndexes(t *testing.T) { + for _, idx := range []string{"results_by_name", "records_by_name"} { + t.Run(idx, func(t *testing.T) { + var isUnique bool + err := rawDB.Raw(` + SELECT i.indisunique + FROM pg_class c + JOIN pg_index i ON i.indexrelid = c.oid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' AND c.relname = ?`, + idx, + ).Row().Scan(&isUnique) + + if err != nil { + t.Fatalf("index %q not found in public schema: %v", idx, err) + } + if !isUnique { + t.Errorf("index %q exists but is not unique", idx) + } + }) + } +} + +// TestSchema_ForeignKeyExists verifies the FK relationship from records → results. +func TestSchema_ForeignKeyExists(t *testing.T) { + var count int64 + err := rawDB.Raw(` + SELECT count(*) + FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'records' + AND constraint_type = 'FOREIGN KEY'`, + ).Scan(&count).Error + + if err != nil { + t.Fatalf("query failed: %v", err) + } + if count == 0 { + t.Error("expected at least one foreign key on the records table") + } +} + +// TestSchema_ForeignKeyCascade verifies that the FK from records → results +// uses ON DELETE CASCADE. The query filters by the specific referenced table +// to be deterministic if additional FKs are added later. +func TestSchema_ForeignKeyCascade(t *testing.T) { + var deleteRule string + err := rawDB.Raw(` + SELECT rc.delete_rule + FROM information_schema.referential_constraints rc + JOIN information_schema.table_constraints tc + ON rc.constraint_name = tc.constraint_name + AND rc.constraint_schema = tc.constraint_schema + JOIN information_schema.constraint_table_usage ctu + ON rc.unique_constraint_name = ctu.constraint_name + AND rc.constraint_schema = ctu.constraint_schema + WHERE tc.table_schema = 'public' + AND tc.table_name = 'records' + AND ctu.table_name = 'results'`, + ).Scan(&deleteRule).Error + + if err != nil { + t.Fatalf("query failed: %v", err) + } + if deleteRule != "CASCADE" { + t.Errorf("expected ON DELETE CASCADE for records→results FK, got %q", deleteRule) + } +} + +// TestSchema_JsonbQueryable verifies that jsonb columns support Postgres-native +// operators. The @> (containment) operator would fail on SQLite TEXT columns. +// This test inserts a row via the deployed API, then queries via raw DB. +func TestSchema_JsonbQueryable(t *testing.T) { + ctx := defaultCtx() + + res, err := adminClient.CreateResult(ctx, &pb.CreateResultRequest{ + Parent: "db-schema-jsonb", + Result: &pb.Result{ + Name: "db-schema-jsonb/results/jsonb-query", + Annotations: map[string]string{"env": "production", "team": "platform"}, + }, + }) + if err != nil { + t.Fatalf("CreateResult failed: %v", err) + } + t.Cleanup(func() { deleteResult(t, res.GetName()) }) + + var count int64 + err = rawDB.Raw( + `SELECT count(*) FROM results WHERE parent = ? AND id = ? AND annotations @> ?`, + "db-schema-jsonb", res.GetUid(), `{"env":"production"}`, + ).Scan(&count).Error + if err != nil { + t.Fatalf("jsonb containment query failed: %v", err) + } + if count == 0 { + t.Error("expected jsonb @> query to find the inserted Result") + } +} diff --git a/test/e2e/e2e.sh b/test/e2e/e2e.sh index 0cd2c1dcfc..725b45a081 100755 --- a/test/e2e/e2e.sh +++ b/test/e2e/e2e.sh @@ -28,6 +28,47 @@ cleanup() { trap cleanup EXIT +run_db_tests() { + REPO="$1" + local LOCAL_PG_PORT=15432 + + echo "Starting Postgres port-forward on localhost:${LOCAL_PG_PORT}..." + kubectl port-forward svc/tekton-results-postgres-service "${LOCAL_PG_PORT}":5432 -n tekton-pipelines & + PF_PID=$! + + # Wait until the forwarded port actually accepts connections instead of + # a blind sleep. Bail after 30 seconds. + for i in $(seq 1 30); do + if bash -c "echo >/dev/tcp/localhost/${LOCAL_PG_PORT}" 2>/dev/null; then + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: port-forward to Postgres did not become ready" + kill "${PF_PID}" 2>/dev/null || true + return 1 + fi + sleep 1 + done + + # Build DB_URL once; suppress set -x so the password stays out of CI logs. + set +x + PGPASS=$(kubectl get secret tekton-results-postgres -n tekton-pipelines \ + -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d) + DB_URL="host=localhost port=${LOCAL_PG_PORT} user=postgres password=${PGPASS} dbname=tekton-results sslmode=disable" + + echo "Running Postgres DB layer e2e tests..." + local test_rc=0 + DB_URL="${DB_URL}" go test -v -count=1 -tags=e2e "${REPO}/test/e2e/db/..." || test_rc=$? + set -x + + kill "${PF_PID}" 2>/dev/null || true + + if [ "${test_rc}" -ne 0 ]; then + echo "ERROR: DB layer e2e tests failed (exit ${test_rc})" + return "${test_rc}" + fi +} + main() { export KO_DOCKER_REPO="kind.local" export KIND_CLUSTER_NAME="tekton-results" @@ -55,18 +96,20 @@ main() { "loglevel.watcher": "debug"} }' kubectl get pod $(kubectl get pod -o=name -n tekton-pipelines | grep tekton-results-watcher | sed "s/^.\{4\}//") -n tekton-pipelines -o yaml - go test -v -count=1 --tags=e2e $(go list --tags=e2e ${REPO}/test/e2e/... | grep -v /client) + go test -v -count=1 --tags=e2e $(go list --tags=e2e ${REPO}/test/e2e/... | grep -v /client | grep -v /db) kubectl logs $(kubectl get pod -o=name -n tekton-pipelines | grep tekton-results-watcher | sed "s/^.\{4\}//") -n tekton-pipelines + # Postgres DB layer tests + run_db_tests "${REPO}" + # Test GCS logging kubectl apply -f ${REPO}/test/e2e/gcs-emulator.yaml kubectl delete pod $(kubectl get pod -o=name -n tekton-pipelines | grep tekton-results-api | sed "s/^.\{4\}//") -n tekton-pipelines kubectl wait deployment "tekton-results-api" --namespace="tekton-pipelines" --for="condition=available" --timeout="120s" kubectl delete pod $(kubectl get pod -o=name -n tekton-pipelines | grep tekton-results-watcher | sed "s/^.\{4\}//") -n tekton-pipelines kubectl wait deployment "tekton-results-watcher" --namespace="tekton-pipelines" --for="condition=available" --timeout="120s" - go test -v -count=1 --tags=e2e,gcs $(go list --tags=e2e ${REPO}/test/e2e/... | grep -v /client) -run TestGCSLog + go test -v -count=1 --tags=e2e,gcs $(go list --tags=e2e ${REPO}/test/e2e/... | grep -v /client | grep -v /db) -run TestGCSLog fi - } main "$@" diff --git a/test/e2e/e2e_gcs_test.go b/test/e2e/e2e_gcs_test.go index e284547e91..e15127e953 100644 --- a/test/e2e/e2e_gcs_test.go +++ b/test/e2e/e2e_gcs_test.go @@ -63,7 +63,7 @@ func TestGCSLog(t *testing.T) { t.Fatalf("Error creating PipelineRun: %v", err) } - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) var logName string diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 93c06e0b28..f8eca88825 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -19,102 +19,45 @@ package e2e import ( "context" - "crypto/tls" "encoding/json" "errors" "io" "net/http" + "os" + "path" "strings" "testing" + "time" "github.com/google/go-cmp/cmp" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + tektonv1client "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1" + resultsv1alpha2 "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" "github.com/tektoncd/results/test/e2e/client" - "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "k8s.io/client-go/rest" - "k8s.io/client-go/transport" - - resultsv1alpha2 "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" - "knative.dev/pkg/apis" - - "time" - - "os" - "path" - - tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - tektonv1client "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/transport" + "knative.dev/pkg/apis" "sigs.k8s.io/yaml" ) const ( - defaultServerName = "tekton-results-api-service.tekton-pipelines.svc.cluster.local" - defaultServerAddress = "https://localhost:8080" - defaultCertFileName = "tekton-results-cert.pem" - allNamespacesReadAccessTokenFileName = "all-namespaces-read-access" - singleNamespaceReadAccessTokenFileName = "single-namespace-read-access" - allNamespacesAdminAccessTokenFileName = "all-namespaces-admin-access" - allNamespacesImpersonateAccessTokenFileName = "all-namespaces-impersonate-access" - defaultCertPath = "/tmp/tekton-results/ssl" - defaultTokenPath = "/tmp/tekton-results/tokens" - defaultNamespace = "default" + allNamespacesReadAccessToken = "all-namespaces-read-access" + singleNamespaceReadAccessToken = "single-namespace-read-access" + allNamespacesAdminAccessToken = "all-namespaces-admin-access" + allNamespacesImpersonateAccessToken = "all-namespaces-impersonate-access" + defaultNamespace = "default" ) -var ( - allNamespacesReadAccessTokenFile, - singleNamespaceReadAccessTokenFile, - allNamespacesAdminAccessTokenFile, - allNamespacesImpersonateAccessTokenFile, - certFile string - serverName string - serverAddress string -) - -//lint:ignore SA1019 - -func init() { - certPath := os.Getenv("SSL_CERT_PATH") - if len(certPath) == 0 { - certPath = defaultCertPath - } - - certFileName := os.Getenv("CERT_FILE_NAME") - if len(certFileName) == 0 { - certFileName = defaultCertFileName - } - certFile = path.Join(certPath, certFileName) - - tokenPath := os.Getenv("SA_TOKEN_PATH") - if len(tokenPath) == 0 { - tokenPath = defaultTokenPath - } - - apiServerName := os.Getenv("API_SERVER_NAME") - if len(apiServerName) == 0 { - apiServerName = defaultServerName - } - serverName = apiServerName - - apiServerAddress := os.Getenv("API_SERVER_ADDR") - if len(apiServerAddress) == 0 { - apiServerAddress = defaultServerAddress - } - serverAddress = apiServerAddress - - allNamespacesReadAccessTokenFile = path.Join(tokenPath, allNamespacesReadAccessTokenFileName) - singleNamespaceReadAccessTokenFile = path.Join(tokenPath, singleNamespaceReadAccessTokenFileName) - allNamespacesAdminAccessTokenFile = path.Join(tokenPath, allNamespacesAdminAccessTokenFileName) - allNamespacesImpersonateAccessTokenFile = path.Join(tokenPath, allNamespacesImpersonateAccessTokenFileName) -} +var envCfg = client.NewEnvConfig() func TestTaskRun(t *testing.T) { ctx := context.Background() @@ -137,7 +80,7 @@ func TestTaskRun(t *testing.T) { t.Fatalf("Error creating TaskRun: %v", err) } - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) var resName, recName, eventName string @@ -228,7 +171,7 @@ func TestPipelineRun(t *testing.T) { t.Fatalf("Error creating PipelineRun: %v", err) } - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) var resName, recName, eventName string @@ -372,57 +315,17 @@ func tektonClient(t *testing.T) *tektonv1client.TektonV1Client { return tektonv1client.NewForConfigOrDie(clientConfig(t)) } -func resultsClient(t *testing.T, tokenFile string, impersonationConfig *transport.ImpersonationConfig) (client.GRPCClient, client.RESTClient) { +func resultsClient(t *testing.T, tokenFileName string, impersonationConfig *transport.ImpersonationConfig) (client.GRPCClient, client.RESTClient) { t.Helper() - if impersonationConfig == nil { - impersonationConfig = &transport.ImpersonationConfig{} - } - - var tlsConfig transport.TLSConfig - transportCredentials, err := credentials.NewClientTLSFromFile(certFile, serverName) - if err != nil { - t.Logf("TLS certificate verification will be skipped, error creating client TLS: %v", err) - transportCredentials = credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}) - tlsConfig = transport.TLSConfig{Insecure: true} - } else { - tlsConfig = transport.TLSConfig{ - CAFile: certFile, - ServerName: serverName, - } - } - - callOptions := []grpc.CallOption{ - grpc.PerRPCCredentials(&client.CustomCredentials{ - TokenSource: transport.NewCachedFileTokenSource(tokenFile), - ImpersonationConfig: impersonationConfig, - }), - } - - grpcOptions := []grpc.DialOption{ - grpc.WithBlock(), //nolint:staticcheck - grpc.WithDefaultCallOptions(callOptions...), - grpc.WithTransportCredentials(transportCredentials), - } - - grpcClient, err := client.NewGRPCClient(serverAddress, grpcOptions...) + grpcClient, err := client.NewGRPCClientFromConfig(envCfg, tokenFileName, impersonationConfig) if err != nil { t.Fatalf("Error creating gRPC client: %v", err) } - restConfig := &transport.Config{ - TLS: tlsConfig, - BearerTokenFile: tokenFile, - Impersonate: *impersonationConfig, - } - - restOptions := []client.RestOption{ - client.WithConfig(restConfig), - } - - restClient, err := client.NewRESTClient(serverAddress, restOptions...) + restClient, err := client.NewRESTClientFromConfig(envCfg, tokenFileName, impersonationConfig) if err != nil { - t.Fatalf("Error creating REST request: %v", err) + t.Fatalf("Error creating REST client: %v", err) } return grpcClient, restClient @@ -439,7 +342,7 @@ func TestGRPCLogging(t *testing.T) { matcher := "\"grpc.method\":\"ListResults\"" - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) t.Run("log entry is found when not expected", func(t *testing.T) { resultsAPILogs, err := getResultsAPILogs(ctx, &podLogOptions, t) @@ -514,7 +417,7 @@ func TestListResults(t *testing.T) { ctx := context.Background() t.Run("list results under the default parent", func(t *testing.T) { - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) res, err := gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: "default"}) if err != nil { @@ -527,7 +430,7 @@ func TestListResults(t *testing.T) { }) t.Run("list results across parents", func(t *testing.T) { - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) // For the purposes of this test suite, listing results under // the `default` parent or using the `-` symbol must return the @@ -556,7 +459,7 @@ func TestListResults(t *testing.T) { }) t.Run("return an error because the identity isn't authorized to access all namespaces", func(t *testing.T) { - gc, _ := resultsClient(t, singleNamespaceReadAccessTokenFile, nil) + gc, _ := resultsClient(t, singleNamespaceReadAccessToken, nil) _, err := gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: "-"}) if err == nil { t.Fatal("Want an unauthenticated error, but the request succeeded") @@ -568,7 +471,7 @@ func TestListResults(t *testing.T) { }) t.Run("list results under the default parent using the identity with more limited access", func(t *testing.T) { - gc, _ := resultsClient(t, singleNamespaceReadAccessTokenFile, nil) + gc, _ := resultsClient(t, singleNamespaceReadAccessToken, nil) res, err := gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: "default"}) if err != nil { t.Fatal(err) @@ -581,7 +484,7 @@ func TestListResults(t *testing.T) { t.Run("grpc and rest consistency", func(t *testing.T) { parent := "default" - gc, rc := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesReadAccessToken, nil) want, err := gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: parent}) if err != nil { t.Fatalf("Error listing Results: %v", err) @@ -602,7 +505,7 @@ func TestListRecords(t *testing.T) { ctx := context.Background() t.Run("list records by omitting the result name", func(t *testing.T) { - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) res, err := gc.ListRecords(ctx, &resultsv1alpha2.ListRecordsRequest{Parent: "default/results/-"}) if err != nil { t.Fatal(err) @@ -614,7 +517,7 @@ func TestListRecords(t *testing.T) { }) t.Run("list records by omitting the parent and result names", func(t *testing.T) { - gc, _ := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, _ := resultsClient(t, allNamespacesReadAccessToken, nil) // For the purposes of this test suite, listing records under // the `default/results/-` result or using the `-/results/-` @@ -644,7 +547,7 @@ func TestListRecords(t *testing.T) { }) t.Run("return an error because the identity isn't authorized to access all namespaces", func(t *testing.T) { - gc, _ := resultsClient(t, singleNamespaceReadAccessTokenFile, nil) + gc, _ := resultsClient(t, singleNamespaceReadAccessToken, nil) _, err := gc.ListRecords(ctx, &resultsv1alpha2.ListRecordsRequest{Parent: "-/results/-"}) if err == nil { t.Fatal("Want an unauthenticated error, but the request succeeded") @@ -655,7 +558,7 @@ func TestListRecords(t *testing.T) { }) t.Run("list records using the identity with more limited access", func(t *testing.T) { - gc, _ := resultsClient(t, singleNamespaceReadAccessTokenFile, nil) + gc, _ := resultsClient(t, singleNamespaceReadAccessToken, nil) resp, err := gc.ListRecords(ctx, &resultsv1alpha2.ListRecordsRequest{Parent: "default/results/-"}) if err != nil { t.Fatal(err) @@ -667,7 +570,7 @@ func TestListRecords(t *testing.T) { t.Run("grpc and rest consistency", func(t *testing.T) { parent := "default/results/-" - gc, rc := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesReadAccessToken, nil) want, err := gc.ListRecords(ctx, &resultsv1alpha2.ListRecordsRequest{Parent: parent}) if err != nil { t.Fatalf("Error listing Records: %v", err) @@ -686,7 +589,7 @@ func TestListRecords(t *testing.T) { func TestGetResult(t *testing.T) { ctx := context.Background() - gc, rc := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesReadAccessToken, nil) list, err := gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: "default"}) if err != nil { @@ -723,7 +626,7 @@ func TestGetResult(t *testing.T) { func TestGetRecord(t *testing.T) { ctx := context.Background() - gc, rc := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesReadAccessToken, nil) list, err := gc.ListRecords(ctx, &resultsv1alpha2.ListRecordsRequest{Parent: "default/results/-"}) if err != nil { @@ -761,7 +664,7 @@ func TestGetRecord(t *testing.T) { func TestDeleteRecord(t *testing.T) { ctx := context.Background() - gc, rc := resultsClient(t, allNamespacesAdminAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesAdminAccessToken, nil) list, err := gc.ListRecords(ctx, &resultsv1alpha2.ListRecordsRequest{Parent: "default/results/-"}) if err != nil { @@ -799,7 +702,7 @@ func TestDeleteRecord(t *testing.T) { func TestDeleteResult(t *testing.T) { ctx := context.Background() - gc, rc := resultsClient(t, allNamespacesAdminAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesAdminAccessToken, nil) list, err := gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: "default"}) if err != nil { @@ -837,15 +740,15 @@ func TestDeleteResult(t *testing.T) { func TestAuthentication(t *testing.T) { ctx := context.Background() - invalidTokenFile := path.Join(defaultTokenPath, "invalid-token") - err := os.WriteFile(invalidTokenFile, []byte("invalid token"), 0666) + const invalidTokenName = "invalid-token" + err := os.WriteFile(path.Join(envCfg.TokenPath, invalidTokenName), []byte("invalid token"), 0666) if err != nil { t.Fatalf("Error writing file: %v", err) } p := "default" t.Run("valid token", func(t *testing.T) { - gc, rc := resultsClient(t, allNamespacesReadAccessTokenFile, nil) + gc, rc := resultsClient(t, allNamespacesReadAccessToken, nil) t.Run("grpc", func(t *testing.T) { _, err = gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: p}) if err != nil { @@ -861,7 +764,7 @@ func TestAuthentication(t *testing.T) { }) t.Run("invalid token", func(t *testing.T) { - gc, rc := resultsClient(t, invalidTokenFile, nil) + gc, rc := resultsClient(t, invalidTokenName, nil) t.Run("grpc", func(t *testing.T) { _, err = gc.ListResults(ctx, &resultsv1alpha2.ListResultsRequest{Parent: p}) if err == nil { @@ -883,7 +786,7 @@ func TestAuthentication(t *testing.T) { func TestAuthorization(t *testing.T) { ctx := context.Background() - gc, rc := resultsClient(t, singleNamespaceReadAccessTokenFile, nil) + gc, rc := resultsClient(t, singleNamespaceReadAccessToken, nil) t.Run("unauthorized token", func(t *testing.T) { p := "tekton" @@ -910,7 +813,7 @@ func TestImpersonation(t *testing.T) { ctx := context.Background() p := "default" t.Run("impersonate with user not having permission", func(t *testing.T) { - gc, rc := resultsClient(t, allNamespacesImpersonateAccessTokenFile, &transport.ImpersonationConfig{ + gc, rc := resultsClient(t, allNamespacesImpersonateAccessToken, &transport.ImpersonationConfig{ UserName: "system:serviceaccount:default:default", }) t.Run("grpc", func(t *testing.T) { @@ -932,7 +835,7 @@ func TestImpersonation(t *testing.T) { }) t.Run("impersonate with user having permission", func(t *testing.T) { - gc, rc := resultsClient(t, allNamespacesImpersonateAccessTokenFile, &transport.ImpersonationConfig{ + gc, rc := resultsClient(t, allNamespacesImpersonateAccessToken, &transport.ImpersonationConfig{ UserName: "system:serviceaccount:default:all-namespaces-read-access", }) t.Run("grpc", func(t *testing.T) {