From 630cbdbcfa56a25de465cb80259fba8f70ff8233 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 7 Mar 2026 17:52:10 +0100 Subject: [PATCH 1/3] test: add MariaDB integration tests for all strfmt types Add database roundtrip tests against MariaDB for all exported format types, exercising the sql.Scanner/driver.Valuer interfaces. Split integration tests into per-database sub-packages (mariadb/, mongodb/) so that tests mutating package-level globals like MarshalFormat cannot interfere with each other. Document the DateTime/MySQL incompatibility (issue #174) in both the test and README, including the MarshalFormat workaround. Also: add MariaDB service to CI workflow, disable gomoddirectives linter (needed for local replace in monorepo setup), fix unused parameter lint in mongo_test.go. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Frederic BIDON --- .github/workflows/integration-test.yml | 15 + .golangci.yml | 1 + README.md | 27 ++ internal/testintegration/go.mod | 4 +- internal/testintegration/go.sum | 4 + .../testintegration/mariadb/mariadb_test.go | 443 ++++++++++++++++++ .../{ => mongodb}/mongo_test.go | 4 +- 7 files changed, 495 insertions(+), 3 deletions(-) create mode 100644 internal/testintegration/mariadb/mariadb_test.go rename internal/testintegration/{ => mongodb}/mongo_test.go (99%) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index ec56b2f..8b11d66 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -20,6 +20,20 @@ jobs: image: mongo:7 ports: - 27017:27017 + mariadb: + image: mariadb:10.11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: strfmt_integration_test + MARIADB_USER: strfmt_test + MARIADB_PASSWORD: strfmt_test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=10s + --health-timeout=5s + --health-retries=5 steps: - uses: actions/checkout@v4 @@ -32,4 +46,5 @@ jobs: working-directory: internal/testintegration env: MONGODB_URI: mongodb://localhost:27017 + MARIADB_DSN: "strfmt_test:strfmt_test@tcp(localhost:3306)/strfmt_integration_test?parseTime=true&loc=UTC" run: go test -tags testintegration -v -count=1 ./... diff --git a/.golangci.yml b/.golangci.yml index dc7c960..3c4cd48 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,6 +4,7 @@ linters: disable: - depguard - funlen + - gomoddirectives - godox - exhaustruct - nlreturn diff --git a/README.md b/README.md index c1ee898..f113d13 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,33 @@ List of defined types: - [UUID7](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7) - [ULID](https://github.com/ulid/spec) +### Database support + +All format types implement the `database/sql` interfaces `sql.Scanner` and `driver.Valuer`, +so they work out of the box with Go's standard `database/sql` package and any SQL driver. + +All format types also implement BSON marshaling/unmarshaling for use with MongoDB +(via [`go.mongodb.org/mongo-driver/v2`](https://pkg.go.dev/go.mongodb.org/mongo-driver/v2)). + +> **MySQL / MariaDB caveat for `DateTime`:** +> The `go-sql-driver/mysql` driver has hard-coded handling for `time.Time` but does not +> intercept type redefinitions like `strfmt.DateTime`. As a result, `DateTime.Value()` sends +> an RFC 3339 string (e.g. `"2024-06-15T12:30:45.123Z"`) that MySQL/MariaDB rejects for +> `DATETIME` columns. +> +> Workaround: set `strfmt.MarshalFormat` to a MySQL-compatible format such as +> `strfmt.ISO8601LocalTime` and normalize to UTC before marshaling: +> +> ```go +> strfmt.MarshalFormat = strfmt.ISO8601LocalTime +> strfmt.NormalizeTimeForMarshal = func(t time.Time) time.Time { return t.UTC() } +> ``` +> +> See [#174](https://github.com/go-openapi/strfmt/issues/174) for details. + +Integration tests for both MongoDB and MariaDB run in CI to verify database roundtrip +compatibility for all format types. See [`internal/testintegration/`](internal/testintegration/). + ## Change log See diff --git a/internal/testintegration/go.mod b/internal/testintegration/go.mod index 7d857a5..65cfba9 100644 --- a/internal/testintegration/go.mod +++ b/internal/testintegration/go.mod @@ -3,11 +3,13 @@ module github.com/go-openapi/strfmt/internal/testintegration go 1.24.0 require ( - github.com/go-openapi/strfmt v0.0.0 + github.com/go-openapi/strfmt v0.25.0 + github.com/go-sql-driver/mysql v1.9.3 go.mongodb.org/mongo-driver/v2 v2.5.0 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/internal/testintegration/go.sum b/internal/testintegration/go.sum index b3ed7af..90bd4ab 100644 --- a/internal/testintegration/go.sum +++ b/internal/testintegration/go.sum @@ -1,9 +1,13 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/internal/testintegration/mariadb/mariadb_test.go b/internal/testintegration/mariadb/mariadb_test.go new file mode 100644 index 0000000..a1387e3 --- /dev/null +++ b/internal/testintegration/mariadb/mariadb_test.go @@ -0,0 +1,443 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +//go:build testintegration + +// Package mariadb_test lives in its own sub-package so that tests which modify +// strfmt globals (e.g. MarshalFormat, NormalizeTimeForMarshal) cannot interfere +// with MongoDB integration tests running in a sibling package. +package mariadb_test + +import ( + "context" + "database/sql" + "encoding/base64" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/go-openapi/strfmt" + _ "github.com/go-sql-driver/mysql" +) + +func mariadbDSN() string { + if dsn := os.Getenv("MARIADB_DSN"); dsn != "" { + return dsn + } + return "strfmt_test:strfmt_test@tcp(localhost:3306)/strfmt_integration_test?parseTime=true&loc=UTC" +} + +func setupMariaDB(t *testing.T) *sql.DB { + t.Helper() + + db, err := sql.Open("mysql", mariadbDSN()) + if err != nil { + t.Fatalf("failed to open MariaDB: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := db.PingContext(ctx); err != nil { + t.Fatalf("failed to ping MariaDB: %v", err) + } + + t.Cleanup(func() { + _ = db.Close() + }) + + return db +} + +// createTable creates a test table and registers cleanup. +// cols is a list of "col_name TYPE" definitions. +func createTable(t *testing.T, db *sql.DB, cols ...string) string { + t.Helper() + + ctx := context.Background() + table := "test_" + strings.ReplaceAll(t.Name(), "/", "_") + ddl := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id VARCHAR(64) PRIMARY KEY, %s)", table, strings.Join(cols, ", ")) + if _, err := db.ExecContext(ctx, ddl); err != nil { + t.Fatalf("CREATE TABLE failed: %v", err) + } + + t.Cleanup(func() { + _, _ = db.ExecContext(context.Background(), "DROP TABLE IF EXISTS "+table) + }) + + return table +} + +// MariaDB time tests: these are the problematic types per issue #174. +// The go-sql-driver/mysql has hard-coded handling for time.Time but not for +// type redefinitions like strfmt.DateTime. The driver.Valuer interface returns +// an RFC3339 string with "Z" suffix that MySQL/MariaDB rejects for DATETIME columns. + +func TestMariaDB_DateTime_AsString(t *testing.T) { + // Workaround: store DateTime as VARCHAR, roundtrip via Value()/Scan() with string. + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value VARCHAR(64)") + + original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC)) + + // Insert using Value() — returns RFC3339 string, VARCHAR column accepts it. + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + var got strfmt.DateTime + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dt1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if !time.Time(original).UTC().Truncate(time.Millisecond).Equal(time.Time(got).UTC().Truncate(time.Millisecond)) { + t.Errorf("DateTime (VARCHAR) roundtrip: got %v, want %v", got, original) + } +} + +func TestMariaDB_DateTime_NativeDatetime_DefaultFormat(t *testing.T) { + // The go-sql-driver/mysql has hard-coded handling for time.Time specifically. + // Since strfmt.DateTime is a type redefinition (type DateTime time.Time), the + // driver doesn't intercept it and falls through to the driver.Valuer interface. + // DateTime.Value() returns an RFC3339 string like "2024-06-15T12:30:45.123Z". + // MySQL/MariaDB rejects this format for DATETIME columns because it doesn't + // understand the "Z" timezone suffix. + // + // See: https://github.com/go-openapi/strfmt/issues/174 + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value DATETIME(3)") + + original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC)) + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original) + if err == nil { + t.Fatal("expected INSERT to fail with default MarshalFormat, but it succeeded") + } + t.Logf("confirmed: default MarshalFormat rejected by MariaDB: %v", err) +} + +func TestMariaDB_DateTime_NativeDatetime_LocalTimeFormat(t *testing.T) { + // Workaround for MySQL/MariaDB DateTime incompatibility (issue #174): + // https://github.com/go-openapi/strfmt/issues/174 + // + // Setting MarshalFormat to ISO8601LocalTime drops the "Z" timezone suffix, + // producing "2024-06-15T12:30:45" which MySQL/MariaDB accepts for DATETIME + // columns. NormalizeTimeForMarshal is set to UTC to ensure consistent + // timezone normalization before the suffix is stripped. + // + // Trade-off: ISO8601LocalTime has second-only precision. For sub-second + // precision, a custom format like "2006-01-02 15:04:05.000" could be used. + // + // NOTE: MarshalFormat is a package-level global — changing it affects all + // strfmt.DateTime instances in the process. This is why the MariaDB tests + // live in a separate sub-package from the MongoDB tests. + savedFormat := strfmt.MarshalFormat + savedNormalize := strfmt.NormalizeTimeForMarshal + t.Cleanup(func() { + strfmt.MarshalFormat = savedFormat + strfmt.NormalizeTimeForMarshal = savedNormalize + }) + + strfmt.MarshalFormat = strfmt.ISO8601LocalTime + strfmt.NormalizeTimeForMarshal = func(t time.Time) time.Time { return t.UTC() } + + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value DATETIME") + + original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC)) + + // With ISO8601LocalTime, Value() returns "2024-06-15T12:30:45" — no "Z", accepted by MySQL. + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + // With parseTime=true, the driver returns time.Time — Scan() handles this. + var got strfmt.DateTime + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dt1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if !time.Time(original).UTC().Truncate(time.Second).Equal(time.Time(got).UTC().Truncate(time.Second)) { + t.Errorf("DateTime (DATETIME) roundtrip: got %v, want %v", time.Time(got).UTC(), time.Time(original).UTC()) + } +} + +func TestMariaDB_Date(t *testing.T) { + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value DATE") + + original := strfmt.Date(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC)) + + // Date.Value() returns "2024-06-15" which MySQL DATE columns accept. + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "d1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + var got strfmt.Date + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "d1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if original.String() != got.String() { + t.Errorf("Date roundtrip: got %v, want %v", got, original) + } +} + +func TestMariaDB_Duration(t *testing.T) { + // Duration.Value() returns int64 (nanoseconds), so use BIGINT column. + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value BIGINT") + + original := strfmt.Duration(42 * time.Second) + + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dur1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + var got strfmt.Duration + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dur1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if original != got { + t.Errorf("Duration roundtrip: got %v, want %v", got, original) + } +} + +// stringRoundTrip is a helper for string-based strfmt types stored in VARCHAR columns. +func stringRoundTrip[T interface { + ~string + fmt.Stringer +}](t *testing.T, db *sql.DB, original T, got *T, +) { + t.Helper() + + ctx := context.Background() + table := createTable(t, db, "value TEXT") + + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "v1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "v1").Scan(got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if original.String() != (*got).String() { + t.Errorf("roundtrip: got %v, want %v", *got, original) + } +} + +func TestMariaDB_URI(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.URI("https://example.com/path?q=1") + var got strfmt.URI + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_Email(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.Email("user@example.com") + var got strfmt.Email + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_Hostname(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.Hostname("example.com") + var got strfmt.Hostname + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_IPv4(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.IPv4("192.168.1.1") + var got strfmt.IPv4 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_IPv6(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.IPv6("::1") + var got strfmt.IPv6 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_CIDR(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.CIDR("192.168.1.0/24") + var got strfmt.CIDR + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_MAC(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.MAC("01:02:03:04:05:06") + var got strfmt.MAC + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_UUID(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e") + var got strfmt.UUID + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_UUID3(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.UUID3("bcd02ab7-6beb-3467-84c0-3bdbea962817") + var got strfmt.UUID3 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_UUID4(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.UUID4("025b0d74-00a2-4885-af46-084e7fbd0701") + var got strfmt.UUID4 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_UUID5(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.UUID5("886313e1-3b8a-5372-9b90-0c9aee199e5d") + var got strfmt.UUID5 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_UUID7(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.UUID7("01943ff8-3e9e-7be4-8921-de6a1e04d599") + var got strfmt.UUID7 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_ISBN(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.ISBN("0321751043") + var got strfmt.ISBN + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_ISBN10(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.ISBN10("0321751043") + var got strfmt.ISBN10 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_ISBN13(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.ISBN13("978-0321751041") + var got strfmt.ISBN13 + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_CreditCard(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.CreditCard("4111-1111-1111-1111") + var got strfmt.CreditCard + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_SSN(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.SSN("111-11-1111") + var got strfmt.SSN + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_HexColor(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.HexColor("#FFFFFF") + var got strfmt.HexColor + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_RGBColor(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.RGBColor("rgb(255,255,255)") + var got strfmt.RGBColor + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_Password(t *testing.T) { + db := setupMariaDB(t) + original := strfmt.Password("super secret stuff here") + var got strfmt.Password + stringRoundTrip(t, db, original, &got) +} + +func TestMariaDB_Base64(t *testing.T) { + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value TEXT") + + payload := []byte("hello world with special chars: éàü") + original := strfmt.Base64(payload) + + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "b64_1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + var got strfmt.Base64 + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "b64_1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if base64.StdEncoding.EncodeToString(original) != base64.StdEncoding.EncodeToString(got) { + t.Errorf("Base64 roundtrip: got %v, want %v", got, original) + } +} + +func TestMariaDB_ULID(t *testing.T) { + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value VARCHAR(64)") + + original, err := strfmt.ParseULID("01ARZ3NDEKTSV4RRFFQ69G5FAV") + if err != nil { + t.Fatalf("failed to parse ULID: %v", err) + } + + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "ulid1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + var got strfmt.ULID + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "ulid1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if original.String() != got.String() { + t.Errorf("ULID roundtrip: got %v, want %v", got, original) + } +} + +func TestMariaDB_ObjectId(t *testing.T) { + db := setupMariaDB(t) + ctx := context.Background() + table := createTable(t, db, "value VARCHAR(64)") + + original := strfmt.NewObjectId("507f1f77bcf86cd799439011") + + if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "oid1", original); err != nil { + t.Fatalf("INSERT failed: %v", err) + } + + var got strfmt.ObjectId + if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "oid1").Scan(&got); err != nil { + t.Fatalf("SELECT/Scan failed: %v", err) + } + + if original != got { + t.Errorf("ObjectId roundtrip: got %v, want %v", got, original) + } +} diff --git a/internal/testintegration/mongo_test.go b/internal/testintegration/mongodb/mongo_test.go similarity index 99% rename from internal/testintegration/mongo_test.go rename to internal/testintegration/mongodb/mongo_test.go index 0c02115..76960d0 100644 --- a/internal/testintegration/mongo_test.go +++ b/internal/testintegration/mongodb/mongo_test.go @@ -3,7 +3,7 @@ //go:build testintegration -package testintegration_test +package mongodb_test import ( "context" @@ -215,7 +215,7 @@ func TestObjectId(t *testing.T) { // stringFormatRoundTrip is a helper for types that serialize as embedded BSON documents // with a "data" string field (most strfmt string-based types). -func stringFormatRoundTrip(t *testing.T, coll *mongo.Collection, id string, input bson.Marshaler, output bson.Unmarshaler, originalStr string) { +func stringFormatRoundTrip(t *testing.T, coll *mongo.Collection, id string, input bson.Marshaler, output bson.Unmarshaler, _ string) { t.Helper() doc := bson.M{"_id": id, "value": input} From 84ede293e451e648dcd8f1a89ba276e734d9e6ea Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 7 Mar 2026 18:05:22 +0100 Subject: [PATCH 2/3] test: add PostgreSQL integration tests, use testify for all integration tests Add PostgreSQL integration test package with 27 tests covering all strfmt types. PostgreSQL handles RFC3339 DateTime natively (no issue #174 workaround needed). Refactor MariaDB and MongoDB tests to use go-openapi/testify/v2. Update CI workflow with PostgreSQL service container. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Frederic BIDON --- .github/workflows/integration-test.yml | 14 + README.md | 2 +- internal/testintegration/go.mod | 5 + internal/testintegration/go.sum | 20 + .../testintegration/mariadb/mariadb_test.go | 141 +++---- .../testintegration/mongodb/mongo_test.go | 206 +++------- .../postgresql/postgresql_test.go | 359 ++++++++++++++++++ 7 files changed, 515 insertions(+), 232 deletions(-) create mode 100644 internal/testintegration/postgresql/postgresql_test.go diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 8b11d66..0634152 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -34,6 +34,19 @@ jobs: --health-interval=10s --health-timeout=5s --health-retries=5 + postgresql: + image: postgres:17 + env: + POSTGRES_USER: strfmt_test + POSTGRES_PASSWORD: strfmt_test + POSTGRES_DB: strfmt_integration_test + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U strfmt_test -d strfmt_integration_test" + --health-interval=10s + --health-timeout=5s + --health-retries=5 steps: - uses: actions/checkout@v4 @@ -47,4 +60,5 @@ jobs: env: MONGODB_URI: mongodb://localhost:27017 MARIADB_DSN: "strfmt_test:strfmt_test@tcp(localhost:3306)/strfmt_integration_test?parseTime=true&loc=UTC" + POSTGRESQL_DSN: "postgres://strfmt_test:strfmt_test@localhost:5432/strfmt_integration_test?sslmode=disable" run: go test -tags testintegration -v -count=1 ./... diff --git a/README.md b/README.md index f113d13..20935fb 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ All format types also implement BSON marshaling/unmarshaling for use with MongoD > > See [#174](https://github.com/go-openapi/strfmt/issues/174) for details. -Integration tests for both MongoDB and MariaDB run in CI to verify database roundtrip +Integration tests for MongoDB, MariaDB, and PostgreSQL run in CI to verify database roundtrip compatibility for all format types. See [`internal/testintegration/`](internal/testintegration/). ## Change log diff --git a/internal/testintegration/go.mod b/internal/testintegration/go.mod index 65cfba9..eb7680d 100644 --- a/internal/testintegration/go.mod +++ b/internal/testintegration/go.mod @@ -4,7 +4,9 @@ go 1.24.0 require ( github.com/go-openapi/strfmt v0.25.0 + github.com/go-openapi/testify/v2 v2.4.0 github.com/go-sql-driver/mysql v1.9.3 + github.com/jackc/pgx/v5 v5.8.0 go.mongodb.org/mongo-driver/v2 v2.5.0 ) @@ -13,6 +15,9 @@ require ( github.com/go-openapi/errors v0.22.7 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.17.6 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/internal/testintegration/go.sum b/internal/testintegration/go.sum index 90bd4ab..ac1aef8 100644 --- a/internal/testintegration/go.sum +++ b/internal/testintegration/go.sum @@ -1,5 +1,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= @@ -14,11 +15,26 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +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/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +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/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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= @@ -61,3 +77,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/testintegration/mariadb/mariadb_test.go b/internal/testintegration/mariadb/mariadb_test.go index a1387e3..3817e71 100644 --- a/internal/testintegration/mariadb/mariadb_test.go +++ b/internal/testintegration/mariadb/mariadb_test.go @@ -19,6 +19,8 @@ import ( "time" "github.com/go-openapi/strfmt" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" _ "github.com/go-sql-driver/mysql" ) @@ -33,16 +35,12 @@ func setupMariaDB(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("mysql", mariadbDSN()) - if err != nil { - t.Fatalf("failed to open MariaDB: %v", err) - } + require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := db.PingContext(ctx); err != nil { - t.Fatalf("failed to ping MariaDB: %v", err) - } + require.NoError(t, db.PingContext(ctx)) t.Cleanup(func() { _ = db.Close() @@ -59,9 +57,8 @@ func createTable(t *testing.T, db *sql.DB, cols ...string) string { ctx := context.Background() table := "test_" + strings.ReplaceAll(t.Name(), "/", "_") ddl := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id VARCHAR(64) PRIMARY KEY, %s)", table, strings.Join(cols, ", ")) - if _, err := db.ExecContext(ctx, ddl); err != nil { - t.Fatalf("CREATE TABLE failed: %v", err) - } + _, err := db.ExecContext(ctx, ddl) + require.NoError(t, err) t.Cleanup(func() { _, _ = db.ExecContext(context.Background(), "DROP TABLE IF EXISTS "+table) @@ -84,18 +81,17 @@ func TestMariaDB_DateTime_AsString(t *testing.T) { original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC)) // Insert using Value() — returns RFC3339 string, VARCHAR column accepts it. - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original) + require.NoError(t, err) var got strfmt.DateTime - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dt1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dt1").Scan(&got) + require.NoError(t, err) - if !time.Time(original).UTC().Truncate(time.Millisecond).Equal(time.Time(got).UTC().Truncate(time.Millisecond)) { - t.Errorf("DateTime (VARCHAR) roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, + time.Time(original).UTC().Truncate(time.Millisecond), + time.Time(got).UTC().Truncate(time.Millisecond), + ) } func TestMariaDB_DateTime_NativeDatetime_DefaultFormat(t *testing.T) { @@ -114,9 +110,7 @@ func TestMariaDB_DateTime_NativeDatetime_DefaultFormat(t *testing.T) { original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC)) _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original) - if err == nil { - t.Fatal("expected INSERT to fail with default MarshalFormat, but it succeeded") - } + require.Error(t, err) t.Logf("confirmed: default MarshalFormat rejected by MariaDB: %v", err) } @@ -152,19 +146,18 @@ func TestMariaDB_DateTime_NativeDatetime_LocalTimeFormat(t *testing.T) { original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC)) // With ISO8601LocalTime, Value() returns "2024-06-15T12:30:45" — no "Z", accepted by MySQL. - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dt1", original) + require.NoError(t, err) // With parseTime=true, the driver returns time.Time — Scan() handles this. var got strfmt.DateTime - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dt1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dt1").Scan(&got) + require.NoError(t, err) - if !time.Time(original).UTC().Truncate(time.Second).Equal(time.Time(got).UTC().Truncate(time.Second)) { - t.Errorf("DateTime (DATETIME) roundtrip: got %v, want %v", time.Time(got).UTC(), time.Time(original).UTC()) - } + assert.EqualT(t, + time.Time(original).UTC().Truncate(time.Second), + time.Time(got).UTC().Truncate(time.Second), + ) } func TestMariaDB_Date(t *testing.T) { @@ -175,18 +168,14 @@ func TestMariaDB_Date(t *testing.T) { original := strfmt.Date(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC)) // Date.Value() returns "2024-06-15" which MySQL DATE columns accept. - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "d1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "d1", original) + require.NoError(t, err) var got strfmt.Date - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "d1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "d1").Scan(&got) + require.NoError(t, err) - if original.String() != got.String() { - t.Errorf("Date roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original.String(), got.String()) } func TestMariaDB_Duration(t *testing.T) { @@ -197,18 +186,14 @@ func TestMariaDB_Duration(t *testing.T) { original := strfmt.Duration(42 * time.Second) - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dur1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "dur1", original) + require.NoError(t, err) var got strfmt.Duration - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dur1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "dur1").Scan(&got) + require.NoError(t, err) - if original != got { - t.Errorf("Duration roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } // stringRoundTrip is a helper for string-based strfmt types stored in VARCHAR columns. @@ -222,17 +207,13 @@ func stringRoundTrip[T interface { ctx := context.Background() table := createTable(t, db, "value TEXT") - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "v1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "v1", original) + require.NoError(t, err) - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "v1").Scan(got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "v1").Scan(got) + require.NoError(t, err) - if original.String() != (*got).String() { - t.Errorf("roundtrip: got %v, want %v", *got, original) - } + assert.EqualT(t, original.String(), (*got).String()) } func TestMariaDB_URI(t *testing.T) { @@ -383,18 +364,14 @@ func TestMariaDB_Base64(t *testing.T) { payload := []byte("hello world with special chars: éàü") original := strfmt.Base64(payload) - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "b64_1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "b64_1", original) + require.NoError(t, err) var got strfmt.Base64 - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "b64_1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "b64_1").Scan(&got) + require.NoError(t, err) - if base64.StdEncoding.EncodeToString(original) != base64.StdEncoding.EncodeToString(got) { - t.Errorf("Base64 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, base64.StdEncoding.EncodeToString(original), base64.StdEncoding.EncodeToString(got)) } func TestMariaDB_ULID(t *testing.T) { @@ -403,22 +380,16 @@ func TestMariaDB_ULID(t *testing.T) { table := createTable(t, db, "value VARCHAR(64)") original, err := strfmt.ParseULID("01ARZ3NDEKTSV4RRFFQ69G5FAV") - if err != nil { - t.Fatalf("failed to parse ULID: %v", err) - } + require.NoError(t, err) - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "ulid1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err = db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "ulid1", original) + require.NoError(t, err) var got strfmt.ULID - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "ulid1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "ulid1").Scan(&got) + require.NoError(t, err) - if original.String() != got.String() { - t.Errorf("ULID roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original.String(), got.String()) } func TestMariaDB_ObjectId(t *testing.T) { @@ -428,16 +399,12 @@ func TestMariaDB_ObjectId(t *testing.T) { original := strfmt.NewObjectId("507f1f77bcf86cd799439011") - if _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "oid1", original); err != nil { - t.Fatalf("INSERT failed: %v", err) - } + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES (?, ?)", table), "oid1", original) + require.NoError(t, err) var got strfmt.ObjectId - if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "oid1").Scan(&got); err != nil { - t.Fatalf("SELECT/Scan failed: %v", err) - } + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = ?", table), "oid1").Scan(&got) + require.NoError(t, err) - if original != got { - t.Errorf("ObjectId roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } diff --git a/internal/testintegration/mongodb/mongo_test.go b/internal/testintegration/mongodb/mongo_test.go index 76960d0..2e6d8f1 100644 --- a/internal/testintegration/mongodb/mongo_test.go +++ b/internal/testintegration/mongodb/mongo_test.go @@ -13,6 +13,8 @@ import ( "time" "github.com/go-openapi/strfmt" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -29,16 +31,12 @@ func setup(t *testing.T) *mongo.Collection { t.Helper() client, err := mongo.Connect(options.Client().ApplyURI(mongoURI())) - if err != nil { - t.Fatalf("failed to connect to MongoDB: %v", err) - } + require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := client.Ping(ctx, nil); err != nil { - t.Fatalf("failed to ping MongoDB: %v", err) - } + require.NoError(t, client.Ping(ctx, nil)) db := client.Database("strfmt_integration_test") coll := db.Collection(t.Name()) @@ -58,15 +56,11 @@ func roundTrip(t *testing.T, coll *mongo.Collection, doc bson.M) bson.M { ctx := context.Background() _, err := coll.InsertOne(ctx, doc) - if err != nil { - t.Fatalf("InsertOne failed: %v", err) - } + require.NoError(t, err) var result bson.M err = coll.FindOne(ctx, bson.M{"_id": doc["_id"]}).Decode(&result) - if err != nil { - t.Fatalf("FindOne failed: %v", err) - } + require.NoError(t, err) return result } @@ -79,21 +73,15 @@ func TestDate(t *testing.T) { result := roundTrip(t, coll, doc) raw, ok := result["value"].(bson.D) - if !ok { - t.Fatalf("expected bson.D for value, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + rawBytes, err := bson.Marshal(raw) - if err != nil { - t.Fatalf("failed to re-marshal: %v", err) - } + require.NoError(t, err) + var got strfmt.Date - if err := bson.Unmarshal(rawBytes, &got); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } + require.NoError(t, bson.Unmarshal(rawBytes, &got)) - if original.String() != got.String() { - t.Errorf("Date roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original.String(), got.String()) } func TestDateTime(t *testing.T) { @@ -105,14 +93,11 @@ func TestDateTime(t *testing.T) { // DateTime uses MarshalBSONValue, so MongoDB stores it as a native datetime. dt, ok := result["value"].(bson.DateTime) - if !ok { - t.Fatalf("expected bson.DateTime, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.DateTime, got %T", result["value"]) + got := strfmt.DateTime(dt.Time()) - if time.Time(original).UTC().UnixMilli() != time.Time(got).UTC().UnixMilli() { - t.Errorf("DateTime roundtrip: got %v, want %v", time.Time(got).UTC(), time.Time(original).UTC()) - } + assert.EqualT(t, time.Time(original).UTC().UnixMilli(), time.Time(got).UTC().UnixMilli()) } func TestDuration(t *testing.T) { @@ -123,21 +108,15 @@ func TestDuration(t *testing.T) { result := roundTrip(t, coll, doc) raw, ok := result["value"].(bson.D) - if !ok { - t.Fatalf("expected bson.D for value, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + rawBytes, err := bson.Marshal(raw) - if err != nil { - t.Fatalf("failed to re-marshal: %v", err) - } + require.NoError(t, err) + var got strfmt.Duration - if err := bson.Unmarshal(rawBytes, &got); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } + require.NoError(t, bson.Unmarshal(rawBytes, &got)) - if original != got { - t.Errorf("Duration roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestBase64(t *testing.T) { @@ -149,49 +128,35 @@ func TestBase64(t *testing.T) { result := roundTrip(t, coll, doc) raw, ok := result["value"].(bson.D) - if !ok { - t.Fatalf("expected bson.D for value, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + rawBytes, err := bson.Marshal(raw) - if err != nil { - t.Fatalf("failed to re-marshal: %v", err) - } + require.NoError(t, err) + var got strfmt.Base64 - if err := bson.Unmarshal(rawBytes, &got); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } + require.NoError(t, bson.Unmarshal(rawBytes, &got)) - if base64.StdEncoding.EncodeToString(original) != base64.StdEncoding.EncodeToString(got) { - t.Errorf("Base64 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, base64.StdEncoding.EncodeToString(original), base64.StdEncoding.EncodeToString(got)) } func TestULID(t *testing.T) { coll := setup(t) original, err := strfmt.ParseULID("01ARZ3NDEKTSV4RRFFQ69G5FAV") - if err != nil { - t.Fatalf("failed to parse ULID: %v", err) - } + require.NoError(t, err) doc := bson.M{"_id": "ulid_test", "value": original} result := roundTrip(t, coll, doc) raw, ok := result["value"].(bson.D) - if !ok { - t.Fatalf("expected bson.D for value, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + rawBytes, err := bson.Marshal(raw) - if err != nil { - t.Fatalf("failed to re-marshal: %v", err) - } + require.NoError(t, err) + var got strfmt.ULID - if err := bson.Unmarshal(rawBytes, &got); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } + require.NoError(t, bson.Unmarshal(rawBytes, &got)) - if original != got { - t.Errorf("ULID roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestObjectId(t *testing.T) { @@ -203,14 +168,11 @@ func TestObjectId(t *testing.T) { // ObjectId uses MarshalBSONValue, so MongoDB stores it as a native ObjectID. oid, ok := result["value"].(bson.ObjectID) - if !ok { - t.Fatalf("expected bson.ObjectID, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.ObjectID, got %T", result["value"]) + got := strfmt.ObjectId(oid) - if original != got { - t.Errorf("ObjectId roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } // stringFormatRoundTrip is a helper for types that serialize as embedded BSON documents @@ -222,16 +184,12 @@ func stringFormatRoundTrip(t *testing.T, coll *mongo.Collection, id string, inpu result := roundTrip(t, coll, doc) raw, ok := result["value"].(bson.D) - if !ok { - t.Fatalf("expected bson.D for value, got %T", result["value"]) - } + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + rawBytes, err := bson.Marshal(raw) - if err != nil { - t.Fatalf("failed to re-marshal: %v", err) - } - if err := bson.Unmarshal(rawBytes, output); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } + require.NoError(t, err) + + require.NoError(t, bson.Unmarshal(rawBytes, output)) } func TestURI(t *testing.T) { @@ -239,9 +197,7 @@ func TestURI(t *testing.T) { original := strfmt.URI("https://example.com/path?q=1") var got strfmt.URI stringFormatRoundTrip(t, coll, "uri_test", original, &got, string(original)) - if original != got { - t.Errorf("URI roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestEmail(t *testing.T) { @@ -249,9 +205,7 @@ func TestEmail(t *testing.T) { original := strfmt.Email("user@example.com") var got strfmt.Email stringFormatRoundTrip(t, coll, "email_test", original, &got, string(original)) - if original != got { - t.Errorf("Email roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestHostname(t *testing.T) { @@ -259,9 +213,7 @@ func TestHostname(t *testing.T) { original := strfmt.Hostname("example.com") var got strfmt.Hostname stringFormatRoundTrip(t, coll, "hostname_test", original, &got, string(original)) - if original != got { - t.Errorf("Hostname roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestIPv4(t *testing.T) { @@ -269,9 +221,7 @@ func TestIPv4(t *testing.T) { original := strfmt.IPv4("192.168.1.1") var got strfmt.IPv4 stringFormatRoundTrip(t, coll, "ipv4_test", original, &got, string(original)) - if original != got { - t.Errorf("IPv4 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestIPv6(t *testing.T) { @@ -279,9 +229,7 @@ func TestIPv6(t *testing.T) { original := strfmt.IPv6("::1") var got strfmt.IPv6 stringFormatRoundTrip(t, coll, "ipv6_test", original, &got, string(original)) - if original != got { - t.Errorf("IPv6 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestCIDR(t *testing.T) { @@ -289,9 +237,7 @@ func TestCIDR(t *testing.T) { original := strfmt.CIDR("192.168.1.0/24") var got strfmt.CIDR stringFormatRoundTrip(t, coll, "cidr_test", original, &got, string(original)) - if original != got { - t.Errorf("CIDR roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestMAC(t *testing.T) { @@ -299,9 +245,7 @@ func TestMAC(t *testing.T) { original := strfmt.MAC("01:02:03:04:05:06") var got strfmt.MAC stringFormatRoundTrip(t, coll, "mac_test", original, &got, string(original)) - if original != got { - t.Errorf("MAC roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestUUID(t *testing.T) { @@ -309,9 +253,7 @@ func TestUUID(t *testing.T) { original := strfmt.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e") var got strfmt.UUID stringFormatRoundTrip(t, coll, "uuid_test", original, &got, string(original)) - if original != got { - t.Errorf("UUID roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestUUID3(t *testing.T) { @@ -319,9 +261,7 @@ func TestUUID3(t *testing.T) { original := strfmt.UUID3("bcd02ab7-6beb-3467-84c0-3bdbea962817") var got strfmt.UUID3 stringFormatRoundTrip(t, coll, "uuid3_test", original, &got, string(original)) - if original != got { - t.Errorf("UUID3 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestUUID4(t *testing.T) { @@ -329,9 +269,7 @@ func TestUUID4(t *testing.T) { original := strfmt.UUID4("025b0d74-00a2-4885-af46-084e7fbd0701") var got strfmt.UUID4 stringFormatRoundTrip(t, coll, "uuid4_test", original, &got, string(original)) - if original != got { - t.Errorf("UUID4 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestUUID5(t *testing.T) { @@ -339,9 +277,7 @@ func TestUUID5(t *testing.T) { original := strfmt.UUID5("886313e1-3b8a-5372-9b90-0c9aee199e5d") var got strfmt.UUID5 stringFormatRoundTrip(t, coll, "uuid5_test", original, &got, string(original)) - if original != got { - t.Errorf("UUID5 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestUUID7(t *testing.T) { @@ -349,9 +285,7 @@ func TestUUID7(t *testing.T) { original := strfmt.UUID7("01943ff8-3e9e-7be4-8921-de6a1e04d599") var got strfmt.UUID7 stringFormatRoundTrip(t, coll, "uuid7_test", original, &got, string(original)) - if original != got { - t.Errorf("UUID7 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestISBN(t *testing.T) { @@ -359,9 +293,7 @@ func TestISBN(t *testing.T) { original := strfmt.ISBN("0321751043") var got strfmt.ISBN stringFormatRoundTrip(t, coll, "isbn_test", original, &got, string(original)) - if original != got { - t.Errorf("ISBN roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestISBN10(t *testing.T) { @@ -369,9 +301,7 @@ func TestISBN10(t *testing.T) { original := strfmt.ISBN10("0321751043") var got strfmt.ISBN10 stringFormatRoundTrip(t, coll, "isbn10_test", original, &got, string(original)) - if original != got { - t.Errorf("ISBN10 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestISBN13(t *testing.T) { @@ -379,9 +309,7 @@ func TestISBN13(t *testing.T) { original := strfmt.ISBN13("978-0321751041") var got strfmt.ISBN13 stringFormatRoundTrip(t, coll, "isbn13_test", original, &got, string(original)) - if original != got { - t.Errorf("ISBN13 roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestCreditCard(t *testing.T) { @@ -389,9 +317,7 @@ func TestCreditCard(t *testing.T) { original := strfmt.CreditCard("4111-1111-1111-1111") var got strfmt.CreditCard stringFormatRoundTrip(t, coll, "creditcard_test", original, &got, string(original)) - if original != got { - t.Errorf("CreditCard roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestSSN(t *testing.T) { @@ -399,9 +325,7 @@ func TestSSN(t *testing.T) { original := strfmt.SSN("111-11-1111") var got strfmt.SSN stringFormatRoundTrip(t, coll, "ssn_test", original, &got, string(original)) - if original != got { - t.Errorf("SSN roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestHexColor(t *testing.T) { @@ -409,9 +333,7 @@ func TestHexColor(t *testing.T) { original := strfmt.HexColor("#FFFFFF") var got strfmt.HexColor stringFormatRoundTrip(t, coll, "hexcolor_test", original, &got, string(original)) - if original != got { - t.Errorf("HexColor roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestRGBColor(t *testing.T) { @@ -419,9 +341,7 @@ func TestRGBColor(t *testing.T) { original := strfmt.RGBColor("rgb(255,255,255)") var got strfmt.RGBColor stringFormatRoundTrip(t, coll, "rgbcolor_test", original, &got, string(original)) - if original != got { - t.Errorf("RGBColor roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } func TestPassword(t *testing.T) { @@ -429,7 +349,5 @@ func TestPassword(t *testing.T) { original := strfmt.Password("super secret stuff here") var got strfmt.Password stringFormatRoundTrip(t, coll, "password_test", original, &got, string(original)) - if original != got { - t.Errorf("Password roundtrip: got %v, want %v", got, original) - } + assert.EqualT(t, original, got) } diff --git a/internal/testintegration/postgresql/postgresql_test.go b/internal/testintegration/postgresql/postgresql_test.go new file mode 100644 index 0000000..c5bd028 --- /dev/null +++ b/internal/testintegration/postgresql/postgresql_test.go @@ -0,0 +1,359 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +//go:build testintegration + +// Package postgresql_test lives in its own sub-package so that tests which +// modify strfmt globals (e.g. MarshalFormat) cannot interfere with other +// integration tests running in sibling packages. +package postgresql_test + +import ( + "context" + "database/sql" + "encoding/base64" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" + _ "github.com/jackc/pgx/v5/stdlib" +) + +func pgDSN() string { + if dsn := os.Getenv("POSTGRESQL_DSN"); dsn != "" { + return dsn + } + return "postgres://strfmt_test:strfmt_test@localhost:5432/strfmt_integration_test?sslmode=disable" +} + +func setupPostgreSQL(t *testing.T) *sql.DB { + t.Helper() + + db, err := sql.Open("pgx", pgDSN()) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + require.NoError(t, db.PingContext(ctx)) + + t.Cleanup(func() { + _ = db.Close() + }) + + return db +} + +// createTable creates a test table and registers cleanup. +// cols is a list of "col_name TYPE" definitions. +func createTable(t *testing.T, db *sql.DB, cols ...string) string { + t.Helper() + + ctx := context.Background() + table := "test_" + strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_")) + ddl := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id VARCHAR(64) PRIMARY KEY, %s)", table, strings.Join(cols, ", ")) + _, err := db.ExecContext(ctx, ddl) + require.NoError(t, err) + + t.Cleanup(func() { + _, _ = db.ExecContext(context.Background(), "DROP TABLE IF EXISTS "+table) + }) + + return table +} + +// PostgreSQL handles RFC3339 timestamps with "Z" suffix natively — unlike +// MySQL/MariaDB, no workaround is needed for DateTime (see issue #174). + +func TestPostgreSQL_DateTime_NativeDatetime(t *testing.T) { + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value TIMESTAMPTZ") + + original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC)) + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "dt1", original) + require.NoError(t, err) + + var got strfmt.DateTime + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "dt1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, + time.Time(original).UTC().Truncate(time.Millisecond), + time.Time(got).UTC().Truncate(time.Millisecond), + ) +} + +func TestPostgreSQL_DateTime_AsString(t *testing.T) { + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value VARCHAR(64)") + + original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC)) + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "dt1", original) + require.NoError(t, err) + + var got strfmt.DateTime + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "dt1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, + time.Time(original).UTC().Truncate(time.Millisecond), + time.Time(got).UTC().Truncate(time.Millisecond), + ) +} + +func TestPostgreSQL_Date(t *testing.T) { + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value DATE") + + original := strfmt.Date(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC)) + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "d1", original) + require.NoError(t, err) + + var got strfmt.Date + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "d1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, original.String(), got.String()) +} + +func TestPostgreSQL_Duration(t *testing.T) { + // Duration.Value() returns int64 (nanoseconds), so use BIGINT column. + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value BIGINT") + + original := strfmt.Duration(42 * time.Second) + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "dur1", original) + require.NoError(t, err) + + var got strfmt.Duration + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "dur1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, original, got) +} + +// stringRoundTrip is a helper for string-based strfmt types stored in TEXT columns. +func stringRoundTrip[T interface { + ~string + fmt.Stringer +}](t *testing.T, db *sql.DB, original T, got *T, +) { + t.Helper() + + ctx := context.Background() + table := createTable(t, db, "value TEXT") + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "v1", original) + require.NoError(t, err) + + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "v1").Scan(got) + require.NoError(t, err) + + assert.EqualT(t, original.String(), (*got).String()) +} + +func TestPostgreSQL_URI(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.URI("https://example.com/path?q=1") + var got strfmt.URI + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_Email(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.Email("user@example.com") + var got strfmt.Email + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_Hostname(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.Hostname("example.com") + var got strfmt.Hostname + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_IPv4(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.IPv4("192.168.1.1") + var got strfmt.IPv4 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_IPv6(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.IPv6("::1") + var got strfmt.IPv6 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_CIDR(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.CIDR("192.168.1.0/24") + var got strfmt.CIDR + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_MAC(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.MAC("01:02:03:04:05:06") + var got strfmt.MAC + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_UUID(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e") + var got strfmt.UUID + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_UUID3(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.UUID3("bcd02ab7-6beb-3467-84c0-3bdbea962817") + var got strfmt.UUID3 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_UUID4(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.UUID4("025b0d74-00a2-4885-af46-084e7fbd0701") + var got strfmt.UUID4 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_UUID5(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.UUID5("886313e1-3b8a-5372-9b90-0c9aee199e5d") + var got strfmt.UUID5 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_UUID7(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.UUID7("01943ff8-3e9e-7be4-8921-de6a1e04d599") + var got strfmt.UUID7 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_ISBN(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.ISBN("0321751043") + var got strfmt.ISBN + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_ISBN10(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.ISBN10("0321751043") + var got strfmt.ISBN10 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_ISBN13(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.ISBN13("978-0321751041") + var got strfmt.ISBN13 + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_CreditCard(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.CreditCard("4111-1111-1111-1111") + var got strfmt.CreditCard + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_SSN(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.SSN("111-11-1111") + var got strfmt.SSN + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_HexColor(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.HexColor("#FFFFFF") + var got strfmt.HexColor + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_RGBColor(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.RGBColor("rgb(255,255,255)") + var got strfmt.RGBColor + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_Password(t *testing.T) { + db := setupPostgreSQL(t) + original := strfmt.Password("super secret stuff here") + var got strfmt.Password + stringRoundTrip(t, db, original, &got) +} + +func TestPostgreSQL_Base64(t *testing.T) { + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value TEXT") + + payload := []byte("hello world with special chars: éàü") + original := strfmt.Base64(payload) + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "b64_1", original) + require.NoError(t, err) + + var got strfmt.Base64 + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "b64_1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, base64.StdEncoding.EncodeToString(original), base64.StdEncoding.EncodeToString(got)) +} + +func TestPostgreSQL_ULID(t *testing.T) { + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value VARCHAR(64)") + + original, err := strfmt.ParseULID("01ARZ3NDEKTSV4RRFFQ69G5FAV") + require.NoError(t, err) + + _, err = db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "ulid1", original) + require.NoError(t, err) + + var got strfmt.ULID + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "ulid1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, original.String(), got.String()) +} + +func TestPostgreSQL_ObjectId(t *testing.T) { + db := setupPostgreSQL(t) + ctx := context.Background() + table := createTable(t, db, "value VARCHAR(64)") + + original := strfmt.NewObjectId("507f1f77bcf86cd799439011") + + _, err := db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (id, value) VALUES ($1, $2)", table), "oid1", original) + require.NoError(t, err) + + var got strfmt.ObjectId + err = db.QueryRowContext(ctx, fmt.Sprintf("SELECT value FROM %s WHERE id = $1", table), "oid1").Scan(&got) + require.NoError(t, err) + + assert.EqualT(t, original, got) +} From ed2f82b11f9901078da8cf9954a2c3c36e3589ae Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 7 Mar 2026 18:07:51 +0100 Subject: [PATCH 3/3] test: wrapped up integration tests against databases Signed-off-by: Frederic BIDON --- internal/testintegration/doc.go | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 internal/testintegration/doc.go diff --git a/internal/testintegration/doc.go b/internal/testintegration/doc.go new file mode 100644 index 0000000..dfca946 --- /dev/null +++ b/internal/testintegration/doc.go @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package testintegration comes as an independent internal module +// to test the serialization of format types against various database backends. +package testintegration