360 lines
12 KiB
Go
360 lines
12 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/getkin/kin-openapi/openapi3"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestInternalOpenAPISpecValidates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loadOpenAPISpec(t)
|
|
}
|
|
|
|
func TestInternalOpenAPISpecFreezesEnsureByEmailRegistrationContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
doc := loadOpenAPISpec(t)
|
|
operation := getOpenAPIOperation(t, doc, "/api/v1/internal/users/ensure-by-email", http.MethodPost)
|
|
|
|
assertSchemaRef(t, requestSchemaRef(t, operation), "#/components/schemas/EnsureByEmailRequest", "ensure-by-email request schema")
|
|
|
|
requestSchema := componentSchemaRef(t, doc, "EnsureByEmailRequest")
|
|
assertRequiredFields(t, requestSchema, "email", "registration_context")
|
|
assertSchemaRef(t, requestSchema.Value.Properties["email"], "#/components/schemas/Email", "ensure-by-email email property")
|
|
assertSchemaRef(t, requestSchema.Value.Properties["registration_context"], "#/components/schemas/RegistrationContext", "ensure-by-email registration_context property")
|
|
require.Contains(t, marshalOpenAPIJSON(t, requestSchema.Value), `"additionalProperties":false`)
|
|
|
|
registrationContext := componentSchemaRef(t, doc, "RegistrationContext")
|
|
assertRequiredFields(t, registrationContext, "preferred_language", "time_zone")
|
|
assertSchemaRef(t, registrationContext.Value.Properties["preferred_language"], "#/components/schemas/LanguageTag", "registration_context preferred_language property")
|
|
assertSchemaRef(t, registrationContext.Value.Properties["time_zone"], "#/components/schemas/TimeZoneName", "registration_context time_zone property")
|
|
require.Contains(t, marshalOpenAPIJSON(t, registrationContext.Value), `"additionalProperties":false`)
|
|
}
|
|
|
|
func TestInternalOpenAPISpecFreezesSharedResponseSchemas(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
doc := loadOpenAPISpec(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
method string
|
|
status int
|
|
wantRef string
|
|
}{
|
|
{
|
|
name: "get my account",
|
|
path: "/api/v1/internal/users/{user_id}/account",
|
|
method: http.MethodGet,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/GetMyAccountResponse",
|
|
},
|
|
{
|
|
name: "update my profile",
|
|
path: "/api/v1/internal/users/{user_id}/profile",
|
|
method: http.MethodPost,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/GetMyAccountResponse",
|
|
},
|
|
{
|
|
name: "update my settings",
|
|
path: "/api/v1/internal/users/{user_id}/settings",
|
|
method: http.MethodPost,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/GetMyAccountResponse",
|
|
},
|
|
{
|
|
name: "get user eligibility",
|
|
path: "/api/v1/internal/users/{user_id}/eligibility",
|
|
method: http.MethodGet,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/UserEligibilityResponse",
|
|
},
|
|
{
|
|
name: "sync declared country",
|
|
path: "/api/v1/internal/users/{user_id}/declared-country/sync",
|
|
method: http.MethodPost,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/DeclaredCountrySyncResponse",
|
|
},
|
|
{
|
|
name: "get user by id",
|
|
path: "/api/v1/internal/users/{user_id}",
|
|
method: http.MethodGet,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/UserLookupResponse",
|
|
},
|
|
{
|
|
name: "get user by email",
|
|
path: "/api/v1/internal/user-lookups/by-email",
|
|
method: http.MethodPost,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/UserLookupResponse",
|
|
},
|
|
{
|
|
name: "get user by user name",
|
|
path: "/api/v1/internal/user-lookups/by-user-name",
|
|
method: http.MethodPost,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/UserLookupResponse",
|
|
},
|
|
{
|
|
name: "list users",
|
|
path: "/api/v1/internal/users",
|
|
method: http.MethodGet,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/UserListResponse",
|
|
},
|
|
{
|
|
name: "delete user",
|
|
path: "/api/v1/internal/users/{user_id}/delete",
|
|
method: http.MethodPost,
|
|
status: http.StatusOK,
|
|
wantRef: "#/components/schemas/DeleteUserResponse",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
operation := getOpenAPIOperation(t, doc, tt.path, tt.method)
|
|
assertSchemaRef(t, responseSchemaRef(t, operation, tt.status), tt.wantRef, tt.name+" response schema")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInternalOpenAPISpecFreezesDeleteUserRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
doc := loadOpenAPISpec(t)
|
|
operation := getOpenAPIOperation(t, doc, "/api/v1/internal/users/{user_id}/delete", http.MethodPost)
|
|
|
|
assertSchemaRef(t, requestSchemaRef(t, operation), "#/components/schemas/DeleteUserRequest", "delete user request schema")
|
|
|
|
requestSchema := componentSchemaRef(t, doc, "DeleteUserRequest")
|
|
assertRequiredFields(t, requestSchema, "reason_code", "actor")
|
|
assertSchemaRef(t, requestSchema.Value.Properties["actor"], "#/components/schemas/ActorRef", "delete user request actor property")
|
|
require.Contains(t, marshalOpenAPIJSON(t, requestSchema.Value), `"additionalProperties":false`)
|
|
|
|
responseSchema := componentSchemaRef(t, doc, "DeleteUserResponse")
|
|
assertRequiredFields(t, responseSchema, "user_id", "deleted_at")
|
|
require.Contains(t, marshalOpenAPIJSON(t, responseSchema.Value), `"additionalProperties":false`)
|
|
}
|
|
|
|
func TestInternalOpenAPISpecSanctionCodeEnumIncludesPermanentBlock(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
doc := loadOpenAPISpec(t)
|
|
schema := componentSchemaRef(t, doc, "SanctionCode")
|
|
|
|
enumValues := make([]string, 0, len(schema.Value.Enum))
|
|
for _, value := range schema.Value.Enum {
|
|
stringValue, ok := value.(string)
|
|
require.True(t, ok, "SanctionCode enum entry must be a string")
|
|
enumValues = append(enumValues, stringValue)
|
|
}
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"login_block",
|
|
"private_game_create_block",
|
|
"private_game_manage_block",
|
|
"game_join_block",
|
|
"profile_update_block",
|
|
"permanent_block",
|
|
}, enumValues)
|
|
}
|
|
|
|
func TestInternalOpenAPISpecErrorEnvelopeRemainsStable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
doc := loadOpenAPISpec(t)
|
|
|
|
errorResponse := componentSchemaRef(t, doc, "ErrorResponse")
|
|
assertRequiredFields(t, errorResponse, "error")
|
|
require.Contains(t, marshalOpenAPIJSON(t, errorResponse.Value), `"additionalProperties":false`)
|
|
assertSchemaRef(t, errorResponse.Value.Properties["error"], "#/components/schemas/ErrorBody", "ErrorResponse error property")
|
|
|
|
errorBody := componentSchemaRef(t, doc, "ErrorBody")
|
|
assertRequiredFields(t, errorBody, "code", "message")
|
|
require.Contains(t, marshalOpenAPIJSON(t, errorBody.Value), `"additionalProperties":false`)
|
|
|
|
require.JSONEq(
|
|
t,
|
|
`{"error":{"code":"invalid_request","message":"request is invalid"}}`,
|
|
string(mustMarshalJSON(t, responseExampleValue(t, doc, "InvalidRequestError", "invalidRequest"))),
|
|
)
|
|
require.JSONEq(
|
|
t,
|
|
`{"error":{"code":"subject_not_found","message":"subject not found"}}`,
|
|
string(mustMarshalJSON(t, responseExampleValue(t, doc, "SubjectNotFoundError", "subjectNotFound"))),
|
|
)
|
|
}
|
|
|
|
func loadOpenAPISpec(t *testing.T) *openapi3.T {
|
|
t.Helper()
|
|
|
|
_, thisFile, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
require.FailNow(t, "runtime.Caller failed")
|
|
}
|
|
|
|
specPath := filepath.Join(filepath.Dir(thisFile), "openapi.yaml")
|
|
loader := openapi3.NewLoader()
|
|
doc, err := loader.LoadFromFile(specPath)
|
|
if err != nil {
|
|
require.Failf(t, "test failed", "load spec %s: %v", specPath, err)
|
|
}
|
|
if doc == nil {
|
|
require.Failf(t, "test failed", "load spec %s: returned nil document", specPath)
|
|
}
|
|
if doc.Info == nil {
|
|
require.Failf(t, "test failed", "load spec %s: missing info section", specPath)
|
|
}
|
|
if doc.Info.Version != "v1" {
|
|
require.Failf(t, "test failed", "spec %s version = %q, want v1", specPath, doc.Info.Version)
|
|
}
|
|
if err := doc.Validate(context.Background()); err != nil {
|
|
require.Failf(t, "test failed", "validate spec %s: %v", specPath, err)
|
|
}
|
|
|
|
return doc
|
|
}
|
|
|
|
func getOpenAPIOperation(t *testing.T, doc *openapi3.T, path string, method string) *openapi3.Operation {
|
|
t.Helper()
|
|
|
|
if doc.Paths == nil {
|
|
require.Failf(t, "test failed", "spec is missing paths while looking up %s %s", method, path)
|
|
}
|
|
pathItem := doc.Paths.Value(path)
|
|
if pathItem == nil {
|
|
require.Failf(t, "test failed", "spec is missing path %s", path)
|
|
}
|
|
operation := pathItem.GetOperation(method)
|
|
if operation == nil {
|
|
require.Failf(t, "test failed", "spec is missing %s operation for path %s", method, path)
|
|
}
|
|
|
|
return operation
|
|
}
|
|
|
|
func requestSchemaRef(t *testing.T, operation *openapi3.Operation) *openapi3.SchemaRef {
|
|
t.Helper()
|
|
|
|
if operation.RequestBody == nil || operation.RequestBody.Value == nil {
|
|
require.FailNow(t, "operation is missing request body")
|
|
}
|
|
mediaType := operation.RequestBody.Value.Content.Get("application/json")
|
|
if mediaType == nil || mediaType.Schema == nil {
|
|
require.FailNow(t, "operation is missing application/json request schema")
|
|
}
|
|
|
|
return mediaType.Schema
|
|
}
|
|
|
|
func responseSchemaRef(t *testing.T, operation *openapi3.Operation, status int) *openapi3.SchemaRef {
|
|
t.Helper()
|
|
|
|
if operation.Responses == nil {
|
|
require.Failf(t, "test failed", "operation is missing responses for status %d", status)
|
|
}
|
|
response := operation.Responses.Status(status)
|
|
if response == nil || response.Value == nil {
|
|
require.Failf(t, "test failed", "operation is missing response for status %d", status)
|
|
}
|
|
mediaType := response.Value.Content.Get("application/json")
|
|
if mediaType == nil || mediaType.Schema == nil {
|
|
require.Failf(t, "test failed", "operation response %d is missing application/json schema", status)
|
|
}
|
|
|
|
return mediaType.Schema
|
|
}
|
|
|
|
func componentSchemaRef(t *testing.T, doc *openapi3.T, name string) *openapi3.SchemaRef {
|
|
t.Helper()
|
|
|
|
if doc.Components == nil {
|
|
require.Failf(t, "test failed", "spec is missing components while looking up schema %s", name)
|
|
}
|
|
schema := doc.Components.Schemas[name]
|
|
if schema == nil || schema.Value == nil {
|
|
require.Failf(t, "test failed", "spec is missing schema %s", name)
|
|
}
|
|
|
|
return schema
|
|
}
|
|
|
|
func responseExampleValue(t *testing.T, doc *openapi3.T, responseName string, exampleName string) any {
|
|
t.Helper()
|
|
|
|
if doc.Components == nil {
|
|
require.Failf(t, "test failed", "spec is missing components while looking up response %s", responseName)
|
|
}
|
|
response := doc.Components.Responses[responseName]
|
|
if response == nil || response.Value == nil {
|
|
require.Failf(t, "test failed", "spec is missing response %s", responseName)
|
|
}
|
|
mediaType := response.Value.Content.Get("application/json")
|
|
if mediaType == nil {
|
|
require.Failf(t, "test failed", "response %s is missing application/json content", responseName)
|
|
}
|
|
example := mediaType.Examples[exampleName]
|
|
if example == nil || example.Value == nil {
|
|
require.Failf(t, "test failed", "response %s is missing example %s", responseName, exampleName)
|
|
}
|
|
|
|
return example.Value.Value
|
|
}
|
|
|
|
func assertSchemaRef(t *testing.T, schemaRef *openapi3.SchemaRef, want string, name string) {
|
|
t.Helper()
|
|
|
|
if schemaRef == nil {
|
|
require.Failf(t, "test failed", "%s schema ref is nil", name)
|
|
}
|
|
if schemaRef.Ref != want {
|
|
require.Failf(t, "test failed", "%s ref = %q, want %q", name, schemaRef.Ref, want)
|
|
}
|
|
}
|
|
|
|
func assertRequiredFields(t *testing.T, schemaRef *openapi3.SchemaRef, fields ...string) {
|
|
t.Helper()
|
|
|
|
required := append([]string(nil), schemaRef.Value.Required...)
|
|
slices.Sort(required)
|
|
want := append([]string(nil), fields...)
|
|
slices.Sort(want)
|
|
if !slices.Equal(required, want) {
|
|
require.Failf(t, "test failed", "schema required fields = %v, want %v", required, want)
|
|
}
|
|
}
|
|
|
|
func mustMarshalJSON(t *testing.T, value any) []byte {
|
|
t.Helper()
|
|
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
require.Failf(t, "test failed", "marshal JSON: %v", err)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
func marshalOpenAPIJSON(t *testing.T, value any) string {
|
|
t.Helper()
|
|
|
|
return string(mustMarshalJSON(t, value))
|
|
}
|