Files
llmgw/terraform-provider-llmgw/internal/client/contract_test.go
T
Ilker ManapandClaude Opus 5.5 15a209e652 Terraform: organizations, teams, projects, keys and memberships
llmgw_org, llmgw_team and llmgw_project rename and move in place;
llmgw_key holds the key in a sensitive secret and is made again when
revoked or expired outside Terraform; llmgw_membership puts a person in
the tree. Tried with OpenTofu against the fake admin API, drift and
import included.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 06:42:07 +02:00

217 lines
7.2 KiB
Go

package client
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
)
// The gateway's OpenAPI document is generated from its route table and the
// Go types its handlers use, and a test there keeps it current. Checking
// this client against it is what keeps the two modules in step without the
// provider importing the gateway.
type document struct {
Paths map[string]map[string]operation `json:"paths"`
Components struct {
Schemas map[string]schemaDoc `json:"schemas"`
} `json:"components"`
}
type operation struct {
RequestBody *struct {
Content map[string]struct {
Schema schemaDoc `json:"schema"`
} `json:"content"`
} `json:"requestBody"`
Responses map[string]struct {
Content map[string]struct {
Schema schemaDoc `json:"schema"`
} `json:"content"`
} `json:"responses"`
}
type schemaDoc struct {
Ref string `json:"$ref"`
Type string `json:"type"`
Items *schemaDoc `json:"items"`
Properties map[string]schemaDoc `json:"properties"`
}
func (s schemaDoc) name() string { return strings.TrimPrefix(s.Ref, "#/components/schemas/") }
func load(t *testing.T) document {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("cannot locate this test")
}
p := filepath.Join(filepath.Dir(file), "..", "..", "..", "docs", "admin-api.openapi.json")
b, err := os.ReadFile(p)
if err != nil {
t.Fatalf("the gateway's OpenAPI document: %v", err)
}
var d document
if err := json.Unmarshal(b, &d); err != nil {
t.Fatal(err)
}
return d
}
func (d document) op(t *testing.T, endpoint string) (operation, bool) {
method, p, _ := strings.Cut(endpoint, " ")
op, ok := d.Paths[p][strings.ToLower(method)]
return op, ok
}
func TestEveryEndpointTheClientCallsIsInTheDocument(t *testing.T) {
d := load(t)
for _, e := range Endpoints {
if _, ok := d.op(t, e); !ok {
t.Errorf("the client calls %s, which the admin API does not serve", e)
}
}
}
// bodies is each endpoint's request and answer: the client's type and the
// schema the document names for it. Empty is none, or one the client
// ignores.
var bodies = []struct {
endpoint string
request any
requestSchema string
answer any
answerSchema string
}{
{"GET /api/v1/models", nil, "", ModelView{}, "ModelView"},
{"POST /api/v1/models", Model{}, "catalog.Model", nil, ""},
{"GET /api/v1/models/{name}", nil, "", Model{}, "catalog.Model"},
{"POST /api/v1/models/{name}/scale", ScaleRequest{}, "ScaleRequest", nil, ""},
{"POST /api/v1/models/{name}/class", ClassRequest{}, "ClassRequest", nil, ""},
{"POST /api/v1/models/{name}/owner", OwnerRequest{}, "OwnerRequest", OwnerView{}, "OwnerView"},
{"POST /api/v1/models/{name}/adapters", AdaptersRequest{}, "AdaptersRequest", nil, ""},
{"GET /api/v1/nodes", nil, "", NodeView{}, "NodeView"},
{"GET /api/v1/quotas", nil, "", QuotaView{}, "QuotaView"},
{"PUT /api/v1/quotas", QuotaRequest{}, "QuotaRequest", QuotaSet{}, "QuotaSet"},
{"GET /api/v1/guard", nil, "", GuardView{}, "GuardView"},
{"POST /api/v1/guard/attachments", GuardAttachmentRequest{}, "GuardAttachmentRequest", Attachment{}, "guard.Attachment"},
{"DELETE /api/v1/guard/attachments", GuardAttachmentRequest{}, "GuardAttachmentRequest", nil, ""},
{"GET /api/v1/orgs", nil, "", OrgView{}, "OrgView"},
{"POST /api/v1/orgs", TenancyCreateRequest{}, "TenancyCreateRequest", TenancyItem{}, "TenancyItem"},
{"PATCH /api/v1/orgs/{id}", TenancyChangeRequest{}, "TenancyChangeRequest", TenancyItem{}, "TenancyItem"},
{"POST /api/v1/teams", TenancyCreateRequest{}, "TenancyCreateRequest", TenancyItem{}, "TenancyItem"},
{"PATCH /api/v1/teams/{id}", TenancyChangeRequest{}, "TenancyChangeRequest", TenancyItem{}, "TenancyItem"},
{"POST /api/v1/projects", TenancyCreateRequest{}, "TenancyCreateRequest", TenancyItem{}, "TenancyItem"},
{"PATCH /api/v1/projects/{id}", TenancyChangeRequest{}, "TenancyChangeRequest", TenancyItem{}, "TenancyItem"},
{"POST /api/v1/keys", KeyCreateRequest{}, "KeyCreateRequest", KeyCreated{}, "KeyCreated"},
{"GET /api/v1/keys/{id}", nil, "", KeyView{}, "KeyView"},
{"GET /api/v1/users/{id}", nil, "", UserView{}, "UserView"},
{"POST /api/v1/users/{id}/memberships", MembershipRequest{}, "MembershipRequest", UserView{}, "UserView"},
{"DELETE /api/v1/users/{id}/memberships", MembershipRequest{}, "MembershipRequest", UserView{}, "UserView"},
}
// nested are the client's types inside those bodies, and their schemas.
var nested = map[reflect.Type]string{
reflect.TypeOf(Profile{}): "catalog.Profile",
reflect.TypeOf(Scaling{}): "catalog.Scaling",
reflect.TypeOf(External{}): "catalog.External",
reflect.TypeOf(Adapter{}): "catalog.Adapter",
reflect.TypeOf(Capabilities{}): "catalog.Capabilities",
reflect.TypeOf(Resources{}): "store.Resources",
reflect.TypeOf(Attachment{}): "GuardAttachmentView",
reflect.TypeOf(TeamView{}): "TeamView",
reflect.TypeOf(ProjectView{}): "ProjectView",
reflect.TypeOf(MembershipView{}): "MembershipView",
}
func TestEveryBodyTheClientSendsAndReadsMatchesTheDocument(t *testing.T) {
d := load(t)
for _, b := range bodies {
op, ok := d.op(t, b.endpoint)
if !ok {
t.Errorf("%s is not in the document", b.endpoint)
continue
}
if b.request != nil {
if op.RequestBody == nil {
t.Errorf("%s takes no body in the document", b.endpoint)
} else if got := op.RequestBody.Content["application/json"].Schema.name(); got != b.requestSchema {
t.Errorf("%s takes %s, not %s", b.endpoint, got, b.requestSchema)
}
checkType(t, d, reflect.TypeOf(b.request), b.requestSchema)
}
if b.answer != nil {
var s schemaDoc
for code, r := range op.Responses {
if strings.HasPrefix(code, "2") {
s = r.Content["application/json"].Schema
}
}
if s.Type == "array" && s.Items != nil {
s = *s.Items
}
if s.name() != b.answerSchema {
t.Errorf("%s answers with %s, not %s", b.endpoint, s.name(), b.answerSchema)
}
checkType(t, d, reflect.TypeOf(b.answer), b.answerSchema)
}
}
for typ, name := range nested {
checkType(t, d, typ, name)
}
}
// checkType fails for each of a struct's JSON keys the schema does not have,
// or has as another kind of value.
func checkType(t *testing.T, d document, typ reflect.Type, schema string) {
t.Helper()
s, ok := d.Components.Schemas[schema]
if !ok {
t.Errorf("no schema %s in the document", schema)
return
}
for i := range typ.NumField() {
f := typ.Field(i)
name, _, _ := strings.Cut(f.Tag.Get("json"), ",")
if name == "" {
name = f.Name
}
p, ok := s.Properties[name]
if !ok {
t.Errorf("%s.%s is %q, which %s does not have", typ.Name(), f.Name, name, schema)
continue
}
if want := kindOf(f.Type); want != "" && p.Type != "" && p.Type != want {
t.Errorf("%s.%s is a %s here and a %s in %s", typ.Name(), f.Name, want, p.Type, schema)
}
}
}
func kindOf(t reflect.Type) string {
if t == reflect.TypeOf(time.Duration(0)) {
return "integer"
}
switch t.Kind() {
case reflect.Pointer:
return kindOf(t.Elem())
case reflect.String:
return "string"
case reflect.Bool:
return "boolean"
case reflect.Int, reflect.Int64:
return "integer"
case reflect.Float64:
return "number"
case reflect.Slice:
return "array"
case reflect.Map:
return "object"
}
return ""
}