17fc813823
M1, M2 and M3 complete * M1, M2 and M3 complete * review changes * docs * docs
596 lines
22 KiB
Go
596 lines
22 KiB
Go
package queryapi_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
queryapi "queryorchestration/api/queryAPI"
|
|
"queryorchestration/internal/cognitoauth"
|
|
"queryorchestration/internal/customschema"
|
|
"queryorchestration/internal/database/repository"
|
|
"queryorchestration/internal/test"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/labstack/echo/v4"
|
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testActorSubject is the fake JWT subject the custom schema handler tests
|
|
// inject into the echo context via ctx.Set("user_claims", ...). The value
|
|
// is an opaque UUID to match the plan's "createdBy is the Cognito subject,
|
|
// not an email" rule.
|
|
const testActorSubject = "b7a4c1d2-8e3f-4a5b-9c6d-0e1f2a3b4c5d"
|
|
|
|
// trivialValidSchemaJSON is a minimal JSON Schema document that passes every
|
|
// rule in customschema.SchemaValidator: draft 2020-12, root type=object,
|
|
// additionalProperties explicitly declared.
|
|
const trivialValidSchemaJSON = `{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"additionalProperties": false
|
|
}`
|
|
|
|
// newCustomSchemaContext creates an echo context with the given method,
|
|
// optional JSON body, and the standard test subject injected as
|
|
// user_claims["sub"]. It is the equivalent of the eula test harness's
|
|
// ctx.Set("user_claims", ...) pattern — there is no package-level
|
|
// SetUserSubject helper, so we mirror the same approach used by
|
|
// eulaHandlers_test.go.
|
|
func newCustomSchemaContext(t testing.TB, method string, body interface{}) (echo.Context, *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
e := echo.New()
|
|
|
|
var reqBody []byte
|
|
if body != nil {
|
|
var err error
|
|
reqBody, err = json.Marshal(body)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
req := httptest.NewRequest(method, "/", bytes.NewReader(reqBody))
|
|
if reqBody != nil {
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
ctx := e.NewContext(req, rec)
|
|
ctx.Set("user_claims", map[string]interface{}{"sub": testActorSubject})
|
|
ctx.Set("user_info", cognitoauth.UserInfo{Username: "admin"})
|
|
return ctx, rec
|
|
}
|
|
|
|
// newCustomSchemaContextRaw is identical to newCustomSchemaContext but
|
|
// accepts a pre-marshaled raw body so tests can send arbitrary JSON with
|
|
// extra fields (e.g., the injected createdBy attack vector).
|
|
func newCustomSchemaContextRaw(t testing.TB, method string, rawBody []byte) (echo.Context, *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
e := echo.New()
|
|
req := httptest.NewRequest(method, "/", bytes.NewReader(rawBody))
|
|
if rawBody != nil {
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
ctx := e.NewContext(req, rec)
|
|
ctx.Set("user_claims", map[string]interface{}{"sub": testActorSubject})
|
|
ctx.Set("user_info", cognitoauth.UserInfo{Username: "admin"})
|
|
return ctx, rec
|
|
}
|
|
|
|
// resetClientForSchemaTests cascades a client and every FK-bound child
|
|
// row out of the database so reruns of the same test start from a clean
|
|
// slate even when the testcontainer Postgres volume is reused across
|
|
// invocations. The delete order follows the FK graph:
|
|
// - documents.custom_schema_id must be NULL'd before we drop schemas
|
|
// - client_metadata_schemas rows must go before documents (audit lineage
|
|
// is independent, so this ordering is driven by the FKs alone)
|
|
// - documents before folders (documents.folderId)
|
|
// - folders before clients (folders.clientid)
|
|
// - clients last
|
|
//
|
|
// We do not use client.Service.HardDelete here because it collects S3
|
|
// paths and runs a full document-cascade pipeline that is unnecessary for
|
|
// schema-handler tests — we just need the rows gone.
|
|
func resetClientForSchemaTests(t testing.TB, cfg *ControllerConfig, clientID string) {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
pool := cfg.GetDBPool()
|
|
|
|
stmts := []string{
|
|
// Delete custom metadata rows first. Trigger 1 (migration 130) blocks
|
|
// UPDATE documents SET custom_schema_id = NULL when metadata rows exist,
|
|
// so we must remove them before clearing the FK column.
|
|
`DELETE FROM document_custom_metadata WHERE document_id IN (SELECT id FROM documents WHERE clientId = $1)`,
|
|
`UPDATE documents SET custom_schema_id = NULL WHERE clientId = $1`,
|
|
`DELETE FROM client_metadata_schemas WHERE client_id = $1`,
|
|
`DELETE FROM documentEntries WHERE documentId IN (SELECT id FROM documents WHERE clientId = $1)`,
|
|
// documentFieldExtractionVersions has a FK referencing documentFieldExtractions;
|
|
// delete versions first, then extractions, then documents.
|
|
`DELETE FROM documentFieldExtractionVersions WHERE documentId IN (SELECT id FROM documents WHERE clientId = $1)`,
|
|
`DELETE FROM documentFieldExtractions WHERE documentId IN (SELECT id FROM documents WHERE clientId = $1)`,
|
|
`DELETE FROM documents WHERE clientId = $1`,
|
|
`DELETE FROM folders WHERE clientId = $1`,
|
|
`DELETE FROM clientCanSync WHERE clientId = $1`,
|
|
`DELETE FROM clients WHERE clientId = $1`,
|
|
}
|
|
for _, stmt := range stmts {
|
|
_, err := pool.Exec(ctx, stmt, clientID)
|
|
require.NoErrorf(t, err, "reset stmt failed: %s", stmt)
|
|
}
|
|
}
|
|
|
|
// mustValidSchema returns a map[string]interface{} that decodes the
|
|
// trivialValidSchemaJSON bytes, so test request bodies can reuse it without
|
|
// repeating the literal.
|
|
func mustValidSchema(t testing.TB) map[string]interface{} {
|
|
t.Helper()
|
|
var m map[string]interface{}
|
|
require.NoError(t, json.Unmarshal([]byte(trivialValidSchemaJSON), &m))
|
|
return m
|
|
}
|
|
|
|
// seedCustomSchema creates a fresh schema row through the real handler so
|
|
// downstream tests get a realistic active v1 to work with. It returns the
|
|
// parsed CustomSchemaResponse.
|
|
func seedCustomSchema(t *testing.T, cons *queryapi.Controllers, clientID, name string) queryapi.CustomSchemaResponse {
|
|
t.Helper()
|
|
descr := "seeded schema " + name
|
|
body := queryapi.CustomSchemaRequest{
|
|
ClientId: clientID,
|
|
Name: name,
|
|
Description: &descr,
|
|
Schema: mustValidSchema(t),
|
|
}
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodPost, body)
|
|
require.NoError(t, cons.CreateCustomSchema(ctx))
|
|
require.Equal(t, http.StatusCreated, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
return resp
|
|
}
|
|
|
|
// ---------- CreateCustomSchema ----------
|
|
|
|
func TestCreateCustomSchema_Handler(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "HAND_CS_C1"
|
|
resetClientForSchemaTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Hand CS Client 1")
|
|
|
|
t.Run("happy_path", func(t *testing.T) {
|
|
descr := "aircraft engineering fields"
|
|
body := queryapi.CustomSchemaRequest{
|
|
ClientId: clientID,
|
|
Name: "aircraft-engineering",
|
|
Description: &descr,
|
|
Schema: mustValidSchema(t),
|
|
}
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodPost, body)
|
|
|
|
require.NoError(t, cons.CreateCustomSchema(ctx))
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
assert.Equal(t, clientID, resp.ClientId)
|
|
assert.Equal(t, "aircraft-engineering", resp.Name)
|
|
assert.Equal(t, int32(1), resp.Version)
|
|
assert.Equal(t, queryapi.SchemaStatusActive, resp.Status)
|
|
assert.Equal(t, testActorSubject, resp.CreatedBy)
|
|
assert.NotEqual(t, uuid.Nil, uuid.UUID(resp.Id))
|
|
|
|
// DB round-trip: the row must exist.
|
|
row, err := cfg.GetDBQueries().GetClientMetadataSchema(t.Context(), uuid.UUID(resp.Id))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, clientID, row.ClientID)
|
|
assert.Equal(t, "aircraft-engineering", row.Name)
|
|
assert.Equal(t, testActorSubject, row.CreatedBy)
|
|
})
|
|
|
|
t.Run("body_created_by_is_ignored", func(t *testing.T) {
|
|
// Attacker sends a createdBy key in the raw body. The generated
|
|
// CustomSchemaRequest type has no CreatedBy field, so c.Bind will
|
|
// silently drop it. The handler must still produce the JWT subject.
|
|
rawBody := []byte(fmt.Sprintf(`{
|
|
"clientId": %q,
|
|
"name": "attacker-created",
|
|
"description": "ignored",
|
|
"schema": %s,
|
|
"createdBy": "attacker-uuid-00000000-0000-0000-0000-000000000000"
|
|
}`, clientID, trivialValidSchemaJSON))
|
|
ctx, rec := newCustomSchemaContextRaw(t, http.MethodPost, rawBody)
|
|
|
|
require.NoError(t, cons.CreateCustomSchema(ctx))
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
assert.Equal(t, testActorSubject, resp.CreatedBy,
|
|
"createdBy must be the JWT subject, never a body-supplied value")
|
|
})
|
|
|
|
t.Run("invalid_schema_rejected", func(t *testing.T) {
|
|
// Missing additionalProperties — validator rule 5.
|
|
invalid := map[string]interface{}{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
}
|
|
body := queryapi.CustomSchemaRequest{
|
|
ClientId: clientID,
|
|
Name: "invalid-schema-case",
|
|
Schema: invalid,
|
|
}
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodPost, body)
|
|
|
|
err := cons.CreateCustomSchema(ctx)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
_ = rec // unused for error-path assertions
|
|
})
|
|
|
|
t.Run("duplicate_name_conflict", func(t *testing.T) {
|
|
// Insert one with a specific name, then try again.
|
|
descr := "first"
|
|
firstBody := queryapi.CustomSchemaRequest{
|
|
ClientId: clientID,
|
|
Name: "duplicate-name-case",
|
|
Description: &descr,
|
|
Schema: mustValidSchema(t),
|
|
}
|
|
ctx1, rec1 := newCustomSchemaContext(t, http.MethodPost, firstBody)
|
|
require.NoError(t, cons.CreateCustomSchema(ctx1))
|
|
require.Equal(t, http.StatusCreated, rec1.Code)
|
|
|
|
ctx2, _ := newCustomSchemaContext(t, http.MethodPost, firstBody)
|
|
err := cons.CreateCustomSchema(ctx2)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusConflict, httpErr.Code)
|
|
})
|
|
|
|
t.Run("oversize_schema_rejected", func(t *testing.T) {
|
|
// Build a valid schema and pad it past the 64 KB ceiling enforced
|
|
// by MaxSchemaDefinitionBytes. We stuff a large description into
|
|
// the properties map so the JSON is still well-formed.
|
|
bigPad := strings.Repeat("x", customschema.MaxSchemaDefinitionBytes)
|
|
oversize := map[string]interface{}{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"description": bigPad,
|
|
}
|
|
body := queryapi.CustomSchemaRequest{
|
|
ClientId: clientID,
|
|
Name: "oversize-case",
|
|
Schema: oversize,
|
|
}
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodPost, body)
|
|
err := cons.CreateCustomSchema(ctx)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
})
|
|
}
|
|
|
|
// ---------- ListCustomSchemas ----------
|
|
|
|
func TestListCustomSchemas_Handler(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "HAND_CS_C2"
|
|
resetClientForSchemaTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Hand CS Client 2")
|
|
|
|
// Seed three schemas: two independent lineages plus a second version of
|
|
// one of them, so default-filter results should return 2 rows (active
|
|
// latest per lineage) and includeAllVersions+status=any should return 3.
|
|
schemaA := seedCustomSchema(t, cons, clientID, "alpha")
|
|
_ = seedCustomSchema(t, cons, clientID, "beta")
|
|
|
|
// Bump alpha to v2 — supersedes v1.
|
|
bumpBody := queryapi.CustomSchemaVersionRequest{Schema: mustValidSchema(t)}
|
|
ctxBump, recBump := newCustomSchemaContext(t, http.MethodPost, bumpBody)
|
|
require.NoError(t, cons.CreateCustomSchemaVersion(ctxBump, schemaA.Id))
|
|
require.Equal(t, http.StatusCreated, recBump.Code)
|
|
|
|
t.Run("default_active_latest_only", func(t *testing.T) {
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
params := queryapi.ListCustomSchemasParams{ClientId: clientID}
|
|
|
|
require.NoError(t, cons.ListCustomSchemas(ctx, params))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaListResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
require.Len(t, resp.Schemas, 2, "default filter should return 2 active-latest rows")
|
|
names := []string{resp.Schemas[0].Name, resp.Schemas[1].Name}
|
|
assert.Contains(t, names, "alpha")
|
|
assert.Contains(t, names, "beta")
|
|
for _, row := range resp.Schemas {
|
|
assert.Equal(t, queryapi.SchemaStatusActive, row.Status)
|
|
}
|
|
})
|
|
|
|
t.Run("status_any_include_all_versions", func(t *testing.T) {
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
status := queryapi.ListCustomSchemasParamsStatusAny
|
|
includeAll := true
|
|
params := queryapi.ListCustomSchemasParams{
|
|
ClientId: clientID,
|
|
Status: &status,
|
|
IncludeAllVersions: &includeAll,
|
|
}
|
|
require.NoError(t, cons.ListCustomSchemas(ctx, params))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaListResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
require.Len(t, resp.Schemas, 3,
|
|
"status=any + includeAllVersions should return alpha v1, alpha v2, and beta v1")
|
|
})
|
|
|
|
t.Run("pagination_limit_one", func(t *testing.T) {
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
limit := int32(1)
|
|
offset := int32(0)
|
|
params := queryapi.ListCustomSchemasParams{
|
|
ClientId: clientID,
|
|
Limit: &limit,
|
|
Offset: &offset,
|
|
}
|
|
require.NoError(t, cons.ListCustomSchemas(ctx, params))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaListResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
require.Len(t, resp.Schemas, 1)
|
|
})
|
|
|
|
t.Run("limit_zero_rejected", func(t *testing.T) {
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
limit := int32(0)
|
|
params := queryapi.ListCustomSchemasParams{ClientId: clientID, Limit: &limit}
|
|
err := cons.ListCustomSchemas(ctx, params)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
})
|
|
|
|
t.Run("limit_over_max_rejected", func(t *testing.T) {
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
limit := int32(201)
|
|
params := queryapi.ListCustomSchemasParams{ClientId: clientID, Limit: &limit}
|
|
err := cons.ListCustomSchemas(ctx, params)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
})
|
|
|
|
t.Run("negative_offset_rejected", func(t *testing.T) {
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
offset := int32(-1)
|
|
params := queryapi.ListCustomSchemasParams{ClientId: clientID, Offset: &offset}
|
|
err := cons.ListCustomSchemas(ctx, params)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
})
|
|
|
|
t.Run("missing_client_id_rejected", func(t *testing.T) {
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
params := queryapi.ListCustomSchemasParams{ClientId: ""}
|
|
err := cons.ListCustomSchemas(ctx, params)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
})
|
|
}
|
|
|
|
// ---------- GetCustomSchema ----------
|
|
|
|
func TestGetCustomSchema_Handler(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "HAND_CS_C3"
|
|
resetClientForSchemaTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Hand CS Client 3")
|
|
|
|
seeded := seedCustomSchema(t, cons, clientID, "get-happy")
|
|
|
|
t.Run("happy_path", func(t *testing.T) {
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
require.NoError(t, cons.GetCustomSchema(ctx, seeded.Id))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
assert.Equal(t, seeded.Id, resp.Id)
|
|
assert.Equal(t, "get-happy", resp.Name)
|
|
assert.NotNil(t, resp.Schema)
|
|
// No documents bound — count must be live 0, canDelete must be true.
|
|
assert.Equal(t, int32(0), resp.DocumentCount)
|
|
assert.True(t, resp.CanDelete)
|
|
})
|
|
|
|
t.Run("missing_id_returns_404", func(t *testing.T) {
|
|
missingID := openapi_types.UUID(uuid.New())
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodGet, nil)
|
|
err := cons.GetCustomSchema(ctx, missingID)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusNotFound, httpErr.Code)
|
|
})
|
|
}
|
|
|
|
// ---------- CreateCustomSchemaVersion ----------
|
|
|
|
func TestCreateCustomSchemaVersion_Handler(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "HAND_CS_C4"
|
|
resetClientForSchemaTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Hand CS Client 4")
|
|
|
|
t.Run("happy_path_bumps_to_v2_and_supersedes_parent", func(t *testing.T) {
|
|
parent := seedCustomSchema(t, cons, clientID, "version-happy")
|
|
|
|
body := queryapi.CustomSchemaVersionRequest{Schema: mustValidSchema(t)}
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodPost, body)
|
|
require.NoError(t, cons.CreateCustomSchemaVersion(ctx, parent.Id))
|
|
require.Equal(t, http.StatusCreated, rec.Code)
|
|
|
|
var resp queryapi.CustomSchemaResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
assert.Equal(t, int32(2), resp.Version)
|
|
assert.Equal(t, queryapi.SchemaStatusActive, resp.Status)
|
|
assert.NotEqual(t, parent.Id, resp.Id, "version bump must produce a new row id")
|
|
|
|
// Parent must now be superseded in the DB.
|
|
parentRow, err := cfg.GetDBQueries().GetClientMetadataSchema(t.Context(), uuid.UUID(parent.Id))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, repository.SchemaStatusTypeSuperseded, parentRow.Status)
|
|
})
|
|
|
|
t.Run("missing_parent_returns_404", func(t *testing.T) {
|
|
body := queryapi.CustomSchemaVersionRequest{Schema: mustValidSchema(t)}
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodPost, body)
|
|
err := cons.CreateCustomSchemaVersion(ctx, openapi_types.UUID(uuid.New()))
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusNotFound, httpErr.Code)
|
|
})
|
|
|
|
t.Run("non_active_parent_returns_409", func(t *testing.T) {
|
|
// Seed v1, bump to v2 (making v1 superseded), then try to version
|
|
// off v1 again.
|
|
parent := seedCustomSchema(t, cons, clientID, "version-nonactive")
|
|
|
|
bumpBody := queryapi.CustomSchemaVersionRequest{Schema: mustValidSchema(t)}
|
|
ctxBump, recBump := newCustomSchemaContext(t, http.MethodPost, bumpBody)
|
|
require.NoError(t, cons.CreateCustomSchemaVersion(ctxBump, parent.Id))
|
|
require.Equal(t, http.StatusCreated, recBump.Code)
|
|
|
|
ctxTry, _ := newCustomSchemaContext(t, http.MethodPost, bumpBody)
|
|
err := cons.CreateCustomSchemaVersion(ctxTry, parent.Id)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusConflict, httpErr.Code)
|
|
})
|
|
}
|
|
|
|
// ---------- DeleteCustomSchema ----------
|
|
|
|
func TestDeleteCustomSchema_Handler(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "HAND_CS_C5"
|
|
resetClientForSchemaTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Hand CS Client 5")
|
|
|
|
t.Run("happy_path_unused_schema", func(t *testing.T) {
|
|
seeded := seedCustomSchema(t, cons, clientID, "delete-happy")
|
|
ctx, rec := newCustomSchemaContext(t, http.MethodDelete, nil)
|
|
require.NoError(t, cons.DeleteCustomSchema(ctx, seeded.Id))
|
|
assert.Equal(t, http.StatusNoContent, rec.Code)
|
|
|
|
// DB: row must now be retired.
|
|
row, err := cfg.GetDBQueries().GetClientMetadataSchema(t.Context(), uuid.UUID(seeded.Id))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, repository.SchemaStatusTypeRetired, row.Status)
|
|
})
|
|
|
|
t.Run("bound_to_document_returns_409", func(t *testing.T) {
|
|
seeded := seedCustomSchema(t, cons, clientID, "delete-bound")
|
|
|
|
// Bind a document to the seeded schema.
|
|
docID, err := cfg.GetDBQueries().CreateDocument(t.Context(), &repository.CreateDocumentParams{
|
|
Clientid: clientID,
|
|
Hash: "delete-bound-hash",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = cfg.GetDBPool().Exec(t.Context(),
|
|
`UPDATE documents SET custom_schema_id = $1 WHERE id = $2`, uuid.UUID(seeded.Id), docID)
|
|
require.NoError(t, err)
|
|
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodDelete, nil)
|
|
err = cons.DeleteCustomSchema(ctx, seeded.Id)
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusConflict, httpErr.Code)
|
|
|
|
// Unbind so subsequent subtests / reruns don't see leftover FK state.
|
|
_, err = cfg.GetDBPool().Exec(t.Context(),
|
|
`UPDATE documents SET custom_schema_id = NULL WHERE id = $1`, docID)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("missing_returns_404", func(t *testing.T) {
|
|
ctx, _ := newCustomSchemaContext(t, http.MethodDelete, nil)
|
|
err := cons.DeleteCustomSchema(ctx, openapi_types.UUID(uuid.New()))
|
|
require.Error(t, err)
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.True(t, ok)
|
|
assert.Equal(t, http.StatusNotFound, httpErr.Code)
|
|
})
|
|
}
|