chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/concurrency"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
"data-terminal-cloud/internal/pagination"
|
||||
"data-terminal-cloud/internal/saas"
|
||||
)
|
||||
|
||||
func TestSaaSInputHelpers(t *testing.T) {
|
||||
if fields := validateSubject("长者", "zh-CN", "Asia/Shanghai", map[string]any{}, map[string]any{}); len(fields) != 0 {
|
||||
t.Fatalf("valid subject fields=%#v", fields)
|
||||
}
|
||||
if fields := validateSubject("", "zh_CN", "Unknown/Zone", nil, nil); len(fields) != 5 {
|
||||
t.Fatalf("invalid subject fields=%#v", fields)
|
||||
}
|
||||
validProfile := map[string]any{"background": "铁路工作经历", "interests": []any{"老照片", "家乡变化"}}
|
||||
validPreferences := map[string]any{"conversation_goal": "聊聊第一次工作", "pace": "slow", "preferred_topics": []any{"工作经历"}}
|
||||
if fields := validateSubject("长者", "zh-CN", "Asia/Shanghai", validProfile, validPreferences); len(fields) != 0 {
|
||||
t.Fatalf("valid conversation context fields=%#v", fields)
|
||||
}
|
||||
invalidProfile := map[string]any{"background": strings.Repeat("长", 501), "interests": []any{"", 1}}
|
||||
invalidPreferences := map[string]any{"conversation_goal": strings.Repeat("长", 201), "pace": "fast", "preferred_topics": strings.Repeat("x", 9)}
|
||||
if fields := validateSubject("长者", "zh-CN", "Asia/Shanghai", invalidProfile, invalidPreferences); len(fields) != 5 {
|
||||
t.Fatalf("invalid conversation context fields=%#v", fields)
|
||||
}
|
||||
if !validLanguage("zh-CN") || validLanguage("zh_CN") || !validTimezone("UTC") || validTimezone("Unknown/Zone") || !validUUID("10000000-0000-0000-0000-000000000001") || validUUID("bad") {
|
||||
t.Fatal("language, timezone or UUID validation contract changed")
|
||||
}
|
||||
fact := saas.CreateFactInput{FactType: " hometown ", Content: " 合成城市 ", SourceType: " model_candidate ", SourceRef: " synthetic://turn/1 ", ConfirmationStatus: " pending ", Sensitivity: " normal "}
|
||||
normalizeCreateFact(&fact)
|
||||
if fields := validateFact(fact.FactType, fact.Content, fact.SourceType, fact.SourceRef, fact.ConfirmationStatus, fact.Sensitivity, nil, nil); len(fields) != 0 {
|
||||
t.Fatalf("valid fact fields=%#v", fields)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
earlier := now.Add(-time.Hour)
|
||||
if fields := validateFact("", "", "unknown", strings.Repeat("x", 301), "confirmed", "unknown", &now, &earlier); len(fields) != 6 {
|
||||
t.Fatalf("invalid fact fields=%#v", fields)
|
||||
}
|
||||
config := map[string]any{"audio": []any{map[string]any{"codec": "opus"}, map[string]any{"api_token": "secret"}}}
|
||||
if path, found := forbiddenDeviceConfigField(config, "desired_config"); !found || path != "desired_config.audio[1].api_token" {
|
||||
t.Fatalf("forbidden config path=%q found=%v", path, found)
|
||||
}
|
||||
if _, found := forbiddenDeviceConfigField(map[string]any{"audio": map[string]any{"codec": "opus"}}, "desired_config"); found {
|
||||
t.Fatal("safe device configuration was rejected")
|
||||
}
|
||||
for _, config := range []map[string]any{
|
||||
{"volume": map[string]any{"speaker": float64(0)}},
|
||||
{"volume": map[string]any{"speaker": float64(65)}},
|
||||
{"volume": map[string]any{"speaker": float64(100)}},
|
||||
{"interaction": map[string]any{"wake_word": "你好"}},
|
||||
} {
|
||||
if fields := validateDesiredDeviceConfig(config); len(fields) != 0 {
|
||||
t.Fatalf("valid desired device config=%#v fields=%#v", config, fields)
|
||||
}
|
||||
}
|
||||
for _, config := range []map[string]any{
|
||||
{"volume": "loud"},
|
||||
{"volume": map[string]any{"speaker": float64(-1)}},
|
||||
{"volume": map[string]any{"speaker": 50.5}},
|
||||
{"volume": map[string]any{"speaker": float64(101)}},
|
||||
{"volume": map[string]any{"speaker": "65"}},
|
||||
} {
|
||||
if fields := validateDesiredDeviceConfig(config); len(fields) != 1 {
|
||||
t.Fatalf("invalid desired device config=%#v fields=%#v", config, fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeAndResourcePrincipal(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"ok"}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if !decodeWebJSON(recorder, request, &body, "请求错误") || body.Name != "ok" {
|
||||
t.Fatalf("decoded body=%#v status=%d", body, recorder.Code)
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"ok","extra":1}`))
|
||||
recorder = httptest.NewRecorder()
|
||||
if decodeWebJSON(recorder, request, &body, "请求错误") || recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("unknown field status=%d", recorder.Code)
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodGet, "/resources/bad", nil)
|
||||
request.SetPathValue("id", "bad")
|
||||
request = request.WithContext(identity.WithPrincipal(request.Context(), identity.Principal{Issuer: "issuer", Subject: "subject"}))
|
||||
recorder = httptest.NewRecorder()
|
||||
if _, ok := resourcePrincipal(recorder, request, "id"); ok || recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("invalid resource principal status=%d", recorder.Code)
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodGet, "/resources/1", nil)
|
||||
recorder = httptest.NewRecorder()
|
||||
if _, ok := resourcePrincipal(recorder, request, "id"); ok || recorder.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("anonymous resource principal status=%d", recorder.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaaSErrorAndETagMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
status int
|
||||
code string
|
||||
}{
|
||||
{saas.ErrAccessDenied, http.StatusForbidden, "RESOURCE_ACCESS_DENIED"},
|
||||
{saas.ErrNotFound, http.StatusNotFound, "RESOURCE_NOT_FOUND"},
|
||||
{saas.ErrVersionConflict, http.StatusPreconditionFailed, "VERSION_CONFLICT"},
|
||||
{saas.ErrEffectiveConsentExist, http.StatusConflict, "EFFECTIVE_CONSENT_EXISTS"},
|
||||
{saas.ErrPolicyVersionMismatch, http.StatusConflict, "POLICY_VERSION_MISMATCH"},
|
||||
{saas.ErrDeviceClaimInvalid, http.StatusConflict, "DEVICE_CLAIM_INVALID"},
|
||||
{saas.ErrDeviceClaimUsed, http.StatusConflict, "DEVICE_CLAIM_ALREADY_USED"},
|
||||
{saas.ErrDeviceAlreadyBound, http.StatusConflict, "DEVICE_ALREADY_BOUND"},
|
||||
{saas.ErrIdempotencyConflict, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT"},
|
||||
{saas.ErrSubjectDeleteConflict, http.StatusConflict, "SUBJECT_ACTIVE_CONVERSATION"},
|
||||
{saas.ErrDeleteConfirmation, http.StatusConflict, "DELETE_CONFIRMATION_MISMATCH"},
|
||||
{saas.ErrExportUnavailable, http.StatusServiceUnavailable, "EXPORT_DOWNLOAD_UNAVAILABLE"},
|
||||
{saas.ErrFirmwareStateConflict, http.StatusConflict, "FIRMWARE_STATE_CONFLICT"},
|
||||
{saas.ErrFirmwareSigningKeyConflict, http.StatusConflict, "FIRMWARE_SIGNING_KEY_CONFLICT"},
|
||||
{saas.ErrFirmwareSigningKeyInUse, http.StatusConflict, "FIRMWARE_SIGNING_KEY_IN_USE"},
|
||||
{saas.ErrFirmwareSigningKeyInactive, http.StatusConflict, "FIRMWARE_SIGNING_KEY_INACTIVE"},
|
||||
{saas.ErrFirmwareReleaseInUse, http.StatusConflict, "FIRMWARE_RELEASE_IN_USE"},
|
||||
{saas.ErrInvitationInvalid, http.StatusConflict, "MEMBER_INVITATION_INVALID"},
|
||||
{saas.ErrMemberExists, http.StatusConflict, "HOUSEHOLD_MEMBER_EXISTS"},
|
||||
{saas.ErrOwnerProtected, http.StatusConflict, "HOUSEHOLD_OWNER_PROTECTED"},
|
||||
{saas.ErrFactSourceRequired, http.StatusConflict, "FACT_SOURCE_REQUIRED"},
|
||||
{saas.ErrStorySourceRequired, http.StatusConflict, "STORY_SOURCE_REQUIRED"},
|
||||
{saas.ErrPersonalizedMemoryConsentNeeded, http.StatusConflict, "PERSONALIZED_MEMORY_CONSENT_REQUIRED"},
|
||||
{saas.ErrReviewStateConflict, http.StatusConflict, "STORY_REVIEW_STATE_CONFLICT"},
|
||||
{pagination.ErrInvalidCursor, http.StatusBadRequest, "VALIDATION_FAILED"},
|
||||
{errors.New("unexpected"), http.StatusInternalServerError, "INTERNAL_ERROR"},
|
||||
}
|
||||
for _, item := range tests {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
writeSaaSError(recorder, request, item.err)
|
||||
var response responseEnvelope
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if recorder.Code != item.status || response.Error == nil || response.Error.Code != item.code {
|
||||
t.Fatalf("error=%v status=%d body=%s", item.err, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
for _, item := range []struct {
|
||||
err error
|
||||
status int
|
||||
}{
|
||||
{concurrency.ErrPreconditionRequired, http.StatusPreconditionRequired},
|
||||
{concurrency.ErrVersionConflict, http.StatusPreconditionFailed},
|
||||
{errors.New("bad etag"), http.StatusBadRequest},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
writeETagError(recorder, httptest.NewRequest(http.MethodPatch, "/", nil), item.err)
|
||||
if recorder.Code != item.status {
|
||||
t.Fatalf("etag error=%v status=%d", item.err, recorder.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user