Loading cmd/felis/apply.go +5 −1 Changes for cmd/felis/apply.go: 5 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -160,6 +160,10 @@ func buildMinecraftServerFromApplyRequest(req applyRequest, namespace string) (* if err := naming.ValidateServerName(req.Subdomain); err != nil { return nil, fmt.Errorf("invalid subdomain: %w", err) } displayName, err := naming.CleanDisplayName(req.DisplayName) if err != nil { return nil, fmt.Errorf("invalid displayName: %w", err) } if strings.TrimSpace(req.Image) == "" { return nil, fmt.Errorf("image is required") } Loading Loading @@ -229,7 +233,7 @@ func buildMinecraftServerFromApplyRequest(req applyRequest, namespace string) (* }, Spec: v1alpha1.MinecraftServerSpec{ Subdomain: req.Subdomain, DisplayName: req.DisplayName, DisplayName: displayName, Image: req.Image, JavaMemory: deriveApplyJavaHeap(memLim), DesiredState: v1alpha1.DesiredStopped, Loading cmd/felis/apply_test.go +9 −0 Changes for cmd/felis/apply_test.go: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -142,6 +142,7 @@ func TestBuildMinecraftServerFromApplyRequest_Valid(t *testing.T) { req := applyRequest{ Name: "test-server", Subdomain: "test-server", DisplayName: " Test Server ", Image: "registry.felis.svc/paper:1.21", Memory: "4Gi", Storage: "20Gi", Loading @@ -159,6 +160,9 @@ func TestBuildMinecraftServerFromApplyRequest_Valid(t *testing.T) { if ms.Spec.Subdomain != "test-server" { t.Errorf("Subdomain = %q", ms.Spec.Subdomain) } if ms.Spec.DisplayName != "Test Server" { t.Errorf("DisplayName = %q, want it trimmed to Test Server", ms.Spec.DisplayName) } if ms.Spec.Image != "registry.felis.svc/paper:1.21" { t.Errorf("Image = %q", ms.Spec.Image) } Loading Loading @@ -283,6 +287,11 @@ func TestBuildMinecraftServerFromApplyRequest_Errors(t *testing.T) { applyRequest{Name: ok, Subdomain: "", Image: "x", Memory: "1Gi", Storage: "1Gi"}, "invalid subdomain", }, { "display name with a tab", applyRequest{Name: ok, Subdomain: ok, DisplayName: "a" + string(rune(0x09)) + "b", Image: "x", Memory: "1Gi", Storage: "1Gi"}, "invalid displayName", }, { "empty image", applyRequest{Name: ok, Subdomain: ok, Image: "", Memory: "1Gi", Storage: "1Gi"}, Loading docs/openapi.yaml +11 −5 Changes for docs/openapi.yaml: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -1106,18 +1106,21 @@ paths: properties: name: { type: string } subdomain: { type: string } display_name: { type: string } displayName: type: string maxLength: 64 description: Trimmed; at most 64 characters, all visible ones or spaces (400 bad_display_name otherwise). image: { type: string } memory: { type: string } storage: { type: string } autostart_policy: { type: string } autostartPolicy: { type: string } resources: type: object properties: cpu: { type: string } cpu_request: { type: string } cpuRequest: { type: string } memory: { type: string } memory_request: { type: string } memoryRequest: { type: string } responses: '201': description: Created; starts Stopped. Loading Loading @@ -6090,7 +6093,10 @@ paths: properties: displayName: type: string description: Trimmed. An empty name clears it, and the server goes by its name again. maxLength: 64 description: >- Trimmed. An empty name clears it, and the server goes by its name again. At most 64 characters, all visible ones or spaces (400 bad_display_name otherwise). autostartPolicy: { type: string } image: type: string Loading internal/api/handlers_create_test.go +30 −0 Changes for internal/api/handlers_create_test.go: 30 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,9 +4,11 @@ import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "felis.lolicon.best/internal/apis/felis/v1alpha1" "felis.lolicon.best/internal/naming" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) Loading Loading @@ -43,6 +45,12 @@ const admittedImage = "registry.felis.svc:5000/mc:1" const validCreateBody = `{"name":"survival","subdomain":"survival",` + `"image":"registry.felis.svc:5000/mc:1","memory":"2Gi","storage":"10Gi"}` // createBodyWithDisplayName is validCreateBody with a displayName, JSON-escaped. func createBodyWithDisplayName(name string) string { quoted, _ := json.Marshal(name) return strings.TrimSuffix(validCreateBody, "}") + `,"displayName":` + string(quoted) + "}" } // TestCreateServerSuccess covers the happy path end-to-end: the form is // validated, the business rows are seeded, the CRD is created cold and unowned, // and the §22 memory ceiling is materialized on the created spec. Loading Loading @@ -195,6 +203,16 @@ func TestCreateServerRejections(t *testing.T) { body: `{"name":"survival","subdomain":"lobby","image":"registry.felis.svc:5000/mc:1","memory":"2Gi","storage":"10Gi"}`, wantCode: http.StatusBadRequest, wantErr: "bad_subdomain", }, { name: "display name too long", body: createBodyWithDisplayName(strings.Repeat("生", naming.MaxDisplayName+1)), wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "display name with a line break", body: createBodyWithDisplayName("Survival" + string(rune(0x0A)) + "Realm"), wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "bad autostart policy", body: `{"name":"survival","subdomain":"survival","image":"registry.felis.svc:5000/mc:1","memory":"2Gi","storage":"10Gi","autostartPolicy":"sometimes"}`, Loading Loading @@ -330,3 +348,15 @@ func TestCreateServerWithoutBuilderIs503(t *testing.T) { t.Error("no CRD may be created without a Builder") } } // The display name is trimmed before it is written, like a patch does. func TestCreateServerTrimsDisplayName(t *testing.T) { api, _, cl, _ := newCreateAPI() w := do(api.ExternalHandler(), "POST", "/api/v1/servers", createBodyWithDisplayName(" Survival Realm "), nil) if w.Code != http.StatusCreated { t.Fatalf("code = %d, want 201 (%s)", w.Code, w.Body.String()) } if got := cl.created["survival"].DisplayName; got != "Survival Realm" { t.Fatalf("created displayName = %q, want %q", got, "Survival Realm") } } internal/api/handlers_patch_test.go +10 −0 Changes for internal/api/handlers_patch_test.go: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -177,6 +177,16 @@ func TestPatchServerRejections(t *testing.T) { body: `{"displayName":"x"}`, wantCode: http.StatusBadRequest, wantErr: "bad_name", }, { name: "display name too long", body: `{"displayName":"` + strings.Repeat("x", 65) + `"}`, wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "display name with a right-to-left override", body: `{"displayName":"abc` + string(rune(0x202E)) + `exe.txt"}`, wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "empty autostart policy", body: `{"autostartPolicy":""}`, Loading Loading
cmd/felis/apply.go +5 −1 Changes for cmd/felis/apply.go: 5 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -160,6 +160,10 @@ func buildMinecraftServerFromApplyRequest(req applyRequest, namespace string) (* if err := naming.ValidateServerName(req.Subdomain); err != nil { return nil, fmt.Errorf("invalid subdomain: %w", err) } displayName, err := naming.CleanDisplayName(req.DisplayName) if err != nil { return nil, fmt.Errorf("invalid displayName: %w", err) } if strings.TrimSpace(req.Image) == "" { return nil, fmt.Errorf("image is required") } Loading Loading @@ -229,7 +233,7 @@ func buildMinecraftServerFromApplyRequest(req applyRequest, namespace string) (* }, Spec: v1alpha1.MinecraftServerSpec{ Subdomain: req.Subdomain, DisplayName: req.DisplayName, DisplayName: displayName, Image: req.Image, JavaMemory: deriveApplyJavaHeap(memLim), DesiredState: v1alpha1.DesiredStopped, Loading
cmd/felis/apply_test.go +9 −0 Changes for cmd/felis/apply_test.go: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -142,6 +142,7 @@ func TestBuildMinecraftServerFromApplyRequest_Valid(t *testing.T) { req := applyRequest{ Name: "test-server", Subdomain: "test-server", DisplayName: " Test Server ", Image: "registry.felis.svc/paper:1.21", Memory: "4Gi", Storage: "20Gi", Loading @@ -159,6 +160,9 @@ func TestBuildMinecraftServerFromApplyRequest_Valid(t *testing.T) { if ms.Spec.Subdomain != "test-server" { t.Errorf("Subdomain = %q", ms.Spec.Subdomain) } if ms.Spec.DisplayName != "Test Server" { t.Errorf("DisplayName = %q, want it trimmed to Test Server", ms.Spec.DisplayName) } if ms.Spec.Image != "registry.felis.svc/paper:1.21" { t.Errorf("Image = %q", ms.Spec.Image) } Loading Loading @@ -283,6 +287,11 @@ func TestBuildMinecraftServerFromApplyRequest_Errors(t *testing.T) { applyRequest{Name: ok, Subdomain: "", Image: "x", Memory: "1Gi", Storage: "1Gi"}, "invalid subdomain", }, { "display name with a tab", applyRequest{Name: ok, Subdomain: ok, DisplayName: "a" + string(rune(0x09)) + "b", Image: "x", Memory: "1Gi", Storage: "1Gi"}, "invalid displayName", }, { "empty image", applyRequest{Name: ok, Subdomain: ok, Image: "", Memory: "1Gi", Storage: "1Gi"}, Loading
docs/openapi.yaml +11 −5 Changes for docs/openapi.yaml: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -1106,18 +1106,21 @@ paths: properties: name: { type: string } subdomain: { type: string } display_name: { type: string } displayName: type: string maxLength: 64 description: Trimmed; at most 64 characters, all visible ones or spaces (400 bad_display_name otherwise). image: { type: string } memory: { type: string } storage: { type: string } autostart_policy: { type: string } autostartPolicy: { type: string } resources: type: object properties: cpu: { type: string } cpu_request: { type: string } cpuRequest: { type: string } memory: { type: string } memory_request: { type: string } memoryRequest: { type: string } responses: '201': description: Created; starts Stopped. Loading Loading @@ -6090,7 +6093,10 @@ paths: properties: displayName: type: string description: Trimmed. An empty name clears it, and the server goes by its name again. maxLength: 64 description: >- Trimmed. An empty name clears it, and the server goes by its name again. At most 64 characters, all visible ones or spaces (400 bad_display_name otherwise). autostartPolicy: { type: string } image: type: string Loading
internal/api/handlers_create_test.go +30 −0 Changes for internal/api/handlers_create_test.go: 30 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,9 +4,11 @@ import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "felis.lolicon.best/internal/apis/felis/v1alpha1" "felis.lolicon.best/internal/naming" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) Loading Loading @@ -43,6 +45,12 @@ const admittedImage = "registry.felis.svc:5000/mc:1" const validCreateBody = `{"name":"survival","subdomain":"survival",` + `"image":"registry.felis.svc:5000/mc:1","memory":"2Gi","storage":"10Gi"}` // createBodyWithDisplayName is validCreateBody with a displayName, JSON-escaped. func createBodyWithDisplayName(name string) string { quoted, _ := json.Marshal(name) return strings.TrimSuffix(validCreateBody, "}") + `,"displayName":` + string(quoted) + "}" } // TestCreateServerSuccess covers the happy path end-to-end: the form is // validated, the business rows are seeded, the CRD is created cold and unowned, // and the §22 memory ceiling is materialized on the created spec. Loading Loading @@ -195,6 +203,16 @@ func TestCreateServerRejections(t *testing.T) { body: `{"name":"survival","subdomain":"lobby","image":"registry.felis.svc:5000/mc:1","memory":"2Gi","storage":"10Gi"}`, wantCode: http.StatusBadRequest, wantErr: "bad_subdomain", }, { name: "display name too long", body: createBodyWithDisplayName(strings.Repeat("生", naming.MaxDisplayName+1)), wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "display name with a line break", body: createBodyWithDisplayName("Survival" + string(rune(0x0A)) + "Realm"), wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "bad autostart policy", body: `{"name":"survival","subdomain":"survival","image":"registry.felis.svc:5000/mc:1","memory":"2Gi","storage":"10Gi","autostartPolicy":"sometimes"}`, Loading Loading @@ -330,3 +348,15 @@ func TestCreateServerWithoutBuilderIs503(t *testing.T) { t.Error("no CRD may be created without a Builder") } } // The display name is trimmed before it is written, like a patch does. func TestCreateServerTrimsDisplayName(t *testing.T) { api, _, cl, _ := newCreateAPI() w := do(api.ExternalHandler(), "POST", "/api/v1/servers", createBodyWithDisplayName(" Survival Realm "), nil) if w.Code != http.StatusCreated { t.Fatalf("code = %d, want 201 (%s)", w.Code, w.Body.String()) } if got := cl.created["survival"].DisplayName; got != "Survival Realm" { t.Fatalf("created displayName = %q, want %q", got, "Survival Realm") } }
internal/api/handlers_patch_test.go +10 −0 Changes for internal/api/handlers_patch_test.go: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -177,6 +177,16 @@ func TestPatchServerRejections(t *testing.T) { body: `{"displayName":"x"}`, wantCode: http.StatusBadRequest, wantErr: "bad_name", }, { name: "display name too long", body: `{"displayName":"` + strings.Repeat("x", 65) + `"}`, wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "display name with a right-to-left override", body: `{"displayName":"abc` + string(rune(0x202E)) + `exe.txt"}`, wantCode: http.StatusBadRequest, wantErr: "bad_display_name", }, { name: "empty autostart policy", body: `{"autostartPolicy":""}`, Loading