Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions internal/plugins/internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package plugins

import (
"context"
"testing"

"github.com/Azure/azqr/internal/models"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

// mockInternalScanner is a minimal InternalPluginScanner implementation for tests.
type mockInternalScanner struct {
metadata PluginMetadata
}

func (m *mockInternalScanner) Scan(_ context.Context, _ azcore.TokenCredential, _ map[string]string, _ *models.ScanParams) ([]ExternalPluginOutput, error) {
return nil, nil
}

func (m *mockInternalScanner) GetMetadata() PluginMetadata {
return m.metadata
}

// mockFlagProviderScanner additionally implements FlagProvider.
type mockFlagProviderScanner struct {
mockInternalScanner
flagRegistered bool
}

func (m *mockFlagProviderScanner) RegisterFlags(cmd *cobra.Command) {
m.flagRegistered = true
cmd.Flags().String("custom-flag", "", "a plugin-specific flag")
}

func TestRegisterInternalPlugin(t *testing.T) {
const name = "test-internal-register"
scanner := &mockInternalScanner{
metadata: PluginMetadata{
Name: name,
Version: "1.0.0",
Description: "internal test plugin",
Type: PluginTypeInternal,
},
}

RegisterInternalPlugin(name, scanner)

// Retrievable via the internal plugin registry.
got, exists := GetInternalPlugin(name)
assert.True(t, exists)
assert.Equal(t, scanner, got)

// Registered with the global plugin registry, with a command attached.
plugin, ok := GetRegistry().Get(name)
assert.True(t, ok)
assert.NotNil(t, plugin.Command)
assert.Equal(t, scanner, plugin.InternalScanner)
assert.Equal(t, "internal test plugin", plugin.Metadata.Description)
}

func TestRegisterInternalPlugin_FlagProvider(t *testing.T) {
const name = "test-internal-flagprovider"
scanner := &mockFlagProviderScanner{
mockInternalScanner: mockInternalScanner{
metadata: PluginMetadata{
Name: name,
Version: "1.0.0",
Description: "flag provider test plugin",
Type: PluginTypeInternal,
},
},
}

RegisterInternalPlugin(name, scanner)

assert.True(t, scanner.flagRegistered, "RegisterFlags should be invoked for FlagProvider scanners")

plugin, ok := GetRegistry().Get(name)
assert.True(t, ok)
assert.NotNil(t, plugin.Command.Flags().Lookup("custom-flag"))
}

func TestGetInternalPlugin_NotFound(t *testing.T) {
got, exists := GetInternalPlugin("does-not-exist")
assert.False(t, exists)
assert.Nil(t, got)
}

func TestCreatePluginCommand(t *testing.T) {
cmd := createPluginCommand("sample", "sample description")

assert.Equal(t, "sample", cmd.Use)
assert.Equal(t, "sample description", cmd.Short)

// All standard scan flags must be present.
standardFlags := []string{
"management-group-id",
"subscription-id",
"resource-group",
"xlsx",
"json",
"csv",
"stdout",
"output-name",
"mask",
"filters",
}
for _, f := range standardFlags {
assert.NotNilf(t, cmd.Flags().Lookup(f), "expected flag %q to be registered", f)
}
}
70 changes: 70 additions & 0 deletions internal/plugins/loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package plugins

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetPluginDirs(t *testing.T) {
dirs := getPluginDirs()

assert.Len(t, dirs, 2)
assert.Equal(t, "./plugins", dirs[1])
assert.True(t, filepath.IsAbs(dirs[0]) || dirs[0] != "",
"first plugin dir should resolve from the user home directory")
assert.Equal(t, filepath.Join(".azqr", "plugins"), filepath.Join(filepath.Base(filepath.Dir(dirs[0])), filepath.Base(dirs[0])))
}

func TestLoadAll_NoPluginDirsReturnsNil(t *testing.T) {
// Isolate from any real plugin directories.
t.Setenv("HOME", t.TempDir())
t.Chdir(t.TempDir())

err := LoadAll()
assert.NoError(t, err)
}

func TestLoadAll_RegistersDiscoveredPlugin(t *testing.T) {
// Isolate home so ~/.azqr/plugins cannot interfere.
t.Setenv("HOME", t.TempDir())

// Create a ./plugins directory with a valid YAML plugin relative to the cwd.
workDir := t.TempDir()
pluginDir := filepath.Join(workDir, "plugins")
if err := os.MkdirAll(pluginDir, 0750); err != nil {
t.Fatalf("failed to create plugin dir: %v", err)
}

const pluginName = "loader-test-plugin"
yamlContent := `---
name: ` + pluginName + `
version: 2.1.0
description: Loader discovery test plugin
queries:
- aprlGuid: loader-guid-001
description: Loader test recommendation
query: |
resources
| project id, name
`
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(yamlContent), 0600); err != nil {
t.Fatalf("failed to write plugin file: %v", err)
}

t.Chdir(workDir)

if err := LoadAll(); err != nil {
t.Fatalf("LoadAll returned error: %v", err)
}

plugin, ok := GetRegistry().Get(pluginName)
assert.True(t, ok, "discovered plugin should be registered")
assert.Equal(t, "2.1.0", plugin.Metadata.Version)
assert.Equal(t, PluginTypeYaml, plugin.Metadata.Type)
}
133 changes: 133 additions & 0 deletions internal/plugins/yaml_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package plugins

import (
"os"
"path/filepath"
"strings"
"testing"
)

// writeYamlPlugin writes content to a temp file and returns its path.
func writeYamlPlugin(t *testing.T, content string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "plugin.yaml")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write yaml plugin: %v", err)
}
return path
}

func TestLoadYamlPlugin_MissingQuery(t *testing.T) {
// Query has neither inline 'query' nor 'queryFile'.
path := writeYamlPlugin(t, `---
name: test-plugin
queries:
- aprlGuid: guid-001
description: A recommendation
`)
_, _, err := LoadYamlPlugin(path)
if err == nil || !strings.Contains(err.Error(), "must have either 'query' or 'queryFile'") {
t.Fatalf("expected query-required error, got %v", err)
}
}

func TestLoadYamlPlugin_MissingAprlGuid(t *testing.T) {
path := writeYamlPlugin(t, `---
name: test-plugin
queries:
- description: A recommendation
query: |
resources | project id
`)
_, _, err := LoadYamlPlugin(path)
if err == nil || !strings.Contains(err.Error(), "aprlGuid") {
t.Fatalf("expected missing aprlGuid error, got %v", err)
}
}

func TestLoadYamlPlugin_MissingDescription(t *testing.T) {
path := writeYamlPlugin(t, `---
name: test-plugin
queries:
- aprlGuid: guid-001
query: |
resources | project id
`)
_, _, err := LoadYamlPlugin(path)
if err == nil || !strings.Contains(err.Error(), "description") {
t.Fatalf("expected missing description error, got %v", err)
}
}

func TestLoadYamlPlugin_MissingQueryFile(t *testing.T) {
// queryFile references a file that does not exist.
dir := t.TempDir()
path := filepath.Join(dir, "plugin.yaml")
content := `---
name: test-plugin
queries:
- aprlGuid: guid-001
description: A recommendation
queryFile: missing.kql
`
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write yaml plugin: %v", err)
}

_, _, err := LoadYamlPlugin(path)
if err == nil || !strings.Contains(err.Error(), "failed to read query file") {
t.Fatalf("expected query file read error, got %v", err)
}
}

func TestLoadYamlPlugin_ExternalQueryFileLoaded(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "query.kql"), []byte("resources | project id, name"), 0600); err != nil {
t.Fatalf("failed to write kql file: %v", err)
}
path := filepath.Join(dir, "plugin.yaml")
content := `---
name: test-plugin
queries:
- aprlGuid: guid-001
description: A recommendation
queryFile: query.kql
`
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write yaml plugin: %v", err)
}

_, recs, err := LoadYamlPlugin(path)
if err != nil {
t.Fatalf("LoadYamlPlugin failed: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 recommendation, got %d", len(recs))
}
if !strings.Contains(recs[0].GraphQuery, "project id, name") {
t.Errorf("expected external kql content to populate GraphQuery, got %q", recs[0].GraphQuery)
}
}

func TestLoadYamlPlugin_VersionDefaulting(t *testing.T) {
// Version omitted should default to 1.0.0.
path := writeYamlPlugin(t, `---
name: test-plugin
queries:
- aprlGuid: guid-001
description: A recommendation
query: |
resources | project id
`)
plugin, _, err := LoadYamlPlugin(path)
if err != nil {
t.Fatalf("LoadYamlPlugin failed: %v", err)
}
if plugin.Metadata.Version != "1.0.0" {
t.Errorf("expected default version 1.0.0, got %q", plugin.Metadata.Version)
}
}
Loading
Loading