-
Notifications
You must be signed in to change notification settings - Fork 9
Add Base64 encoding support for profile content and improve edge-to-edge mobile UI #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f589892
65a0ef3
041702b
4e2ac5d
abb1626
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "crowdsec-manager/internal/config" | ||
| "crowdsec-manager/internal/database" | ||
| "crowdsec-manager/internal/docker" | ||
| "crowdsec-manager/internal/models" | ||
| ) | ||
|
|
||
| func TestGetProfilesPathUsesCrowdSecConfigDir(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| cfg := &config.Config{ | ||
| ConfigDir: filepath.Join("tmp", "config"), | ||
| CrowdSecAcquisFile: filepath.Join("etc", "crowdsec", "acquis.yaml"), | ||
| } | ||
|
|
||
| got := getProfilesPath(cfg) | ||
| want := filepath.Join("tmp", "config", "crowdsec", "profiles.yaml") | ||
| if got != want { | ||
| t.Fatalf("getProfilesPath() = %q, want %q", got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestUpdateProfilesWritesToCrowdSecConfigDir(t *testing.T) { | ||
| configDir := t.TempDir() | ||
| acquisDir := filepath.Join(t.TempDir(), "etc", "crowdsec") | ||
| cfg := &config.Config{ | ||
| ConfigDir: configDir, | ||
| CrowdSecAcquisFile: filepath.Join(acquisDir, "acquis.yaml"), | ||
| CrowdsecContainerName: "crowdsec", | ||
| } | ||
|
|
||
| db, err := database.New(filepath.Join(t.TempDir(), "test.db")) | ||
| if err != nil { | ||
| t.Fatalf("create database: %v", err) | ||
| } | ||
| t.Cleanup(func() { | ||
| if err := db.Close(); err != nil { | ||
| t.Fatalf("close database: %v", err) | ||
| } | ||
| }) | ||
|
|
||
| content := "name: test_profile\nfilters:\n - Alert.Remediation == true\n" | ||
| body, err := json.Marshal(models.ProfileRequest{Content: content}) | ||
| if err != nil { | ||
| t.Fatalf("marshal request: %v", err) | ||
| } | ||
|
|
||
| r := newTestRouter() | ||
| r.PUT("/profiles", UpdateProfiles(db, cfg, &docker.Client{})) | ||
| req := httptest.NewRequest(http.MethodPut, "/profiles", bytes.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| w := httptest.NewRecorder() | ||
| r.ServeHTTP(w, req) | ||
|
|
||
| if w.Code != http.StatusOK { | ||
| t.Fatalf("UpdateProfiles status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) | ||
| } | ||
|
|
||
| writtenPath := filepath.Join(configDir, "crowdsec", "profiles.yaml") | ||
| written, err := os.ReadFile(writtenPath) | ||
| if err != nil { | ||
| t.Fatalf("read written profiles file: %v", err) | ||
| } | ||
| if string(written) != content { | ||
| t.Fatalf("written profiles content = %q, want %q", string(written), content) | ||
| } | ||
|
|
||
| legacyPath := filepath.Join(acquisDir, "profiles.yaml") | ||
| if _, err := os.Stat(legacyPath); !os.IsNotExist(err) { | ||
| t.Fatalf("legacy profiles path stat error = %v, want not exist", err) | ||
| } | ||
|
|
||
| history, err := db.GetLatestProfileHistory() | ||
| if err != nil { | ||
| t.Fatalf("get latest profile history: %v", err) | ||
| } | ||
| if history == nil || history.Content != content { | ||
| t.Fatalf("latest profile history = %#v, want content %q", history, content) | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| func TestUpdateProfilesHandlesBase64Content(t *testing.T) { | ||
| configDir := t.TempDir() | ||
| acquisDir := filepath.Join(t.TempDir(), "etc", "crowdsec") | ||
| cfg := &config.Config{ | ||
| ConfigDir: configDir, | ||
| CrowdSecAcquisFile: filepath.Join(acquisDir, "acquis.yaml"), | ||
| CrowdsecContainerName: "crowdsec", | ||
| } | ||
|
|
||
| db, err := database.New(filepath.Join(t.TempDir(), "test.db")) | ||
| if err != nil { | ||
| t.Fatalf("create database: %v", err) | ||
| } | ||
| t.Cleanup(func() { | ||
| if err := db.Close(); err != nil { | ||
| t.Fatalf("close database: %v", err) | ||
| } | ||
| }) | ||
|
|
||
| content := "name: test_profile_b64\nfilters:\n - Alert.Remediation == false\n" | ||
| b64Content := base64.StdEncoding.EncodeToString([]byte(content)) | ||
| body, err := json.Marshal(models.ProfileRequest{ | ||
| ContentB64: b64Content, | ||
| Encoding: "base64", | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("marshal request: %v", err) | ||
| } | ||
|
|
||
| r := newTestRouter() | ||
| r.PUT("/profiles", UpdateProfiles(db, cfg, &docker.Client{})) | ||
| req := httptest.NewRequest(http.MethodPut, "/profiles", bytes.NewReader(body)) | ||
| req.Header.Set("Content-Type", "application/json") | ||
| w := httptest.NewRecorder() | ||
| r.ServeHTTP(w, req) | ||
|
|
||
| if w.Code != http.StatusOK { | ||
| t.Fatalf("UpdateProfiles status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) | ||
| } | ||
|
|
||
| writtenPath := filepath.Join(configDir, "crowdsec", "profiles.yaml") | ||
| written, err := os.ReadFile(writtenPath) | ||
| if err != nil { | ||
| t.Fatalf("read written profiles file: %v", err) | ||
| } | ||
| if string(written) != content { | ||
| t.Fatalf("written profiles content = %q, want %q", string(written), content) | ||
| } | ||
|
|
||
| history, err := db.GetLatestProfileHistory() | ||
| if err != nil { | ||
| t.Fatalf("get latest profile history: %v", err) | ||
| } | ||
| if history == nil || history.Content != content { | ||
| t.Fatalf("latest profile history = %#v, want content %q", history, content) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "crowdsec-manager/internal/models" | ||
| ) | ||
|
|
||
| func TestDecodeProfileContent(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| content := "filters:\n - Alert.Remediation == true && Alert.GetScope() == \"Ip\"\n" | ||
| encoded := base64.StdEncoding.EncodeToString([]byte(content)) | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| req models.ProfileRequest | ||
| want string | ||
| wantErr error | ||
| wantBase64Err bool | ||
| }{ | ||
| { | ||
| name: "legacy plain content", | ||
| req: models.ProfileRequest{ | ||
| Content: content, | ||
| }, | ||
| want: content, | ||
| }, | ||
| { | ||
| name: "base64 content", | ||
| req: models.ProfileRequest{ | ||
| ContentB64: encoded, | ||
| Encoding: "base64", | ||
| }, | ||
| want: content, | ||
| }, | ||
| { | ||
| name: "base64 content without explicit encoding", | ||
| req: models.ProfileRequest{ | ||
| ContentB64: encoded, | ||
| }, | ||
| want: content, | ||
| }, | ||
| { | ||
| name: "invalid base64", | ||
| req: models.ProfileRequest{ | ||
| ContentB64: "not-valid-base64", | ||
| Encoding: "base64", | ||
| }, | ||
| wantBase64Err: true, | ||
| }, | ||
| { | ||
| name: "missing content", | ||
| req: models.ProfileRequest{}, | ||
| wantErr: errProfileContentMissing, | ||
| }, | ||
| { | ||
| name: "unsupported encoding", | ||
| req: models.ProfileRequest{ | ||
| ContentB64: encoded, | ||
| Encoding: "gzip", | ||
| }, | ||
| wantErr: errProfileEncodingUnsupported, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| got, err := decodeProfileContent(tt.req) | ||
| if tt.wantBase64Err { | ||
| var corruptErr base64.CorruptInputError | ||
| if !errors.As(err, &corruptErr) { | ||
| t.Fatalf("decodeProfileContent() error = %v, want base64.CorruptInputError", err) | ||
| } | ||
| return | ||
| } | ||
| if tt.wantErr != nil { | ||
| if !errors.Is(err, tt.wantErr) { | ||
| t.Fatalf("decodeProfileContent() error = %v, want %v", err, tt.wantErr) | ||
| } | ||
| return | ||
| } | ||
| if err != nil { | ||
| t.Fatalf("decodeProfileContent() unexpected error = %v", err) | ||
| } | ||
| if got != tt.want { | ||
| t.Fatalf("decodeProfileContent() = %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Placeholder for the project's TDD-gate hook. Sits outside any Gradle source | ||
| // set so it is not compiled. The real test for MainActivity belongs in | ||
| // app/src/test/java/com/crowdsec/manager/mobile/ — add when the activity grows | ||
| // non-trivial behavior beyond the BridgeActivity edge-to-edge bootstrap. | ||
|
Comment on lines
+1
to
+4
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | 💤 Low value Consider replacing this placeholder with a documentation file or actual test scaffolding. A non-compiling
The current approach may confuse developers or trigger IDE warnings. 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.