package integrationv2 import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "testing" "cloud.google.com/go/pubsub" "sigs.k8s.io/kustomize/kyaml/kio" "edge-infra.dev/pkg/k8s/testing/kmp" "edge-infra.dev/test/f2" "edge-infra.dev/test/f2/x/ktest" "edge-infra.dev/test/f2/x/postgres" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( httpDomain = "http://" ) type rulesEnginePortForwardKey struct{} type urlElems []string var ( addCommandsURLElems = urlElems{"admin", "commands"} addPrivilegesURLElems = urlElems{"admin", "privileges"} addDefaultRulesURLElems = urlElems{"admin", "rules", "default", "commands"} ) // waits on deployment defined by the manifest func waitOn(tManifest string) f2.StepFn { return func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) manifests, err := processManifests(tManifest, k.Namespace) assert.NoError(t, err) for _, manifest := range manifests { if manifest.GetKind() != "Deployment" { continue } k.WaitOn(t, k.Check(manifest, kmp.IsCurrent())) } return ctx } } // deploys a service to an f2 cluster using ktest func createService(tManifest string, filters ...kio.Filter) f2.StepFn { return func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) manifests, err := processManifests(tManifest, k.Namespace, filters...) assert.NoError(t, err) for _, manifest := range manifests { if manifest.GetKind() == "Namespace" { continue } err = k.Client.Create(ctx, manifest) assert.NoError(t, err) } return ctx } } type addNamePayload struct { Name string `json:"name"` } func addNames(ctx f2.Context, t *testing.T, names []string, urlElems urlElems) (*http.Response, error) { var payload []addNamePayload for _, name := range names { payload = append(payload, addNamePayload{name}) } data, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to unmarshal payload: %w", err) } requestURL, err := createURLFromPortForward(ctx, t, urlElems) if err != nil { return nil, fmt.Errorf("failed to create request URL: %w", err) } resp, err := sendPostRequest(ctx, t, data, requestURL.String()) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } return resp, nil } func addCommands(commands []string) f2.StepFn { return func(ctx f2.Context, t *testing.T) f2.Context { resp, err := addNames(ctx, t, commands, addCommandsURLElems) require.NoError(t, err, "addCommands failed") require.Equal(t, http.StatusOK, resp.StatusCode, "addCommands API returned non-ok status: %d", resp.StatusCode) return ctx } } func addPrivileges(privileges []string) f2.StepFn { return func(ctx f2.Context, t *testing.T) f2.Context { resp, err := addNames(ctx, t, privileges, addPrivilegesURLElems) require.NoError(t, err, "addPrivileges failed") require.Equal(t, http.StatusOK, resp.StatusCode, "addPrivileges API returned non-ok status: %d", resp.StatusCode) return ctx } } type defaultRule struct { command string privs []string } func addDefaultRules(rules []defaultRule) f2.StepFn { type Payload struct { Command string `json:"command"` Privileges []string `json:"privileges"` } return func(ctx f2.Context, t *testing.T) f2.Context { var payload []Payload for _, rule := range rules { payload = append(payload, Payload{rule.command, rule.privs}) } data, err := json.Marshal(payload) require.NoError(t, err, "failed to unmarshal AddDefaultRules payload") requestURL, err := createURLFromPortForward(ctx, t, addDefaultRulesURLElems) require.NoError(t, err, "failed to create valid AddDefaultRules request url") resp, err := sendPostRequest(ctx, t, data, requestURL.String()) require.NoError(t, err, "AddDefaultRules request fail") require.Equal(t, http.StatusOK, resp.StatusCode, "AddDefaultRules API returned non-ok status: %d", resp.StatusCode) return ctx } } func deleteDefaultRule(command, privilege string) f2.StepFn { return func(ctx f2.Context, t *testing.T) f2.Context { portForward, ok := ctx.Context.Value(rulesEnginePortForwardKey{}).(ktest.PortForward) require.True(t, ok, "failed to retrieve portForward struct from ctx") domain := httpDomain + portForward.Retrieve(t) requestURL, err := url.JoinPath(domain, "admin", "rules", "default", "commands", command, "privileges", privilege) require.NoError(t, err, "failed to create valid DeleteDefaultRule request url") resp, err := sendPostRequest(ctx, t, nil, requestURL) require.NoError(t, err, "DeleteDefaultRule request fail") require.Equal(t, http.StatusOK, resp.StatusCode, "DeleteDefaultRule API returned non-ok status: %d", resp.StatusCode) return ctx } } func sendPostRequest(ctx f2.Context, t *testing.T, data []byte, requestURL string) (*http.Response, error) { resp, err := sendRequest(ctx, t, data, requestURL, "POST") if err != nil { return nil, err } _, err = readResponseBody(t, resp) if err != nil { return nil, err } return resp, err } // this is here in order to close the body as well as printing for debugging purposes. func readResponseBody(t *testing.T, resp *http.Response) ([]byte, error) { bytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if len(bytes) != 0 { t.Logf("Response body: %s", string(bytes)) } defer resp.Body.Close() return bytes, nil } func sendRequest(ctx f2.Context, t *testing.T, data []byte, requestURL string, method string) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, method, requestURL, bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } t.Logf("Invoking RCLI services: %s", requestURL) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request") } return resp, nil } func createURLFromPortForward(ctx f2.Context, t *testing.T, urlElems urlElems) (*url.URL, error) { portForward, ok := ctx.Context.Value(rulesEnginePortForwardKey{}).(ktest.PortForward) if !ok { return nil, errors.New("failed to retrieve portForward struct from ctx") } domain, err := url.Parse(httpDomain + portForward.Retrieve(t)) if err != nil { return nil, fmt.Errorf("failed to parse url domain: %w", err) } requestURL := domain.JoinPath(urlElems...) return requestURL, nil } // Takes a map of BslRole to human readable eaprivs (slice). // Uses the postgres extension to connect to the database and run the appropriate query to // insert the data in the table. // Will fail on conflict (duplicate value in table). // Will fail on missing privilege in ea_rules_privileges. func addRoleMapping(roleEaMap map[string][]string) f2.StepFn { return func(ctx f2.Context, t *testing.T) f2.Context { // insert the privs into oi_role_privileges // we could optimise this by using a concatenated query instead but since this is a test db it isn't necessary. pg := postgres.FromContextT(ctx, t) db := pg.DB() for bslRole, eaPrivs := range roleEaMap { for _, priv := range eaPrivs { _, err := db.ExecContext(ctx, InsertIntoEaRulesPrivileges, bslRole, priv) require.NoError(t, err, "issue when inserting roles into ea_roles_privileges (role: %s, privilege: %s)", bslRole, priv) } } return ctx } } const ( // InsertIntoEaRulesPrivileges throws error on no privilegeID found (insert NULL error) InsertIntoEaRulesPrivileges = ` INSERT INTO oi_role_privileges (role_name,privilege_id) select $1, privilege_id from unnest(ARRAY[$2]) as privileges(name) left join ea_rules_privileges on privileges.name = ea_rules_privileges.name ;` ) func assertPubSubMessageAttributesEqual(t *testing.T, msg *pubsub.Message, expectedAttributes map[string]string) { for key, expectedValue := range expectedAttributes { actualValue, exists := msg.Attributes[key] assert.True(t, exists, "expected attribute %s to exist", key) assert.Equal(t, expectedValue, actualValue, "expected attribute %s to have value %s, but got %s", key, expectedValue, actualValue) } } func assertPubSubMessageAttributesNotNil(t *testing.T, msg *pubsub.Message, expectedAttributes []string) { for _, key := range expectedAttributes { value, exists := msg.Attributes[key] assert.True(t, exists, "expected attribute %s to exist", key) assert.NotNil(t, value, "expected attribute %s to be not nil", key) } }