diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be3a0e3..8cf198d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,8 @@ name: testing on: pull_request: branches: [master] + push: + branches: [master] workflow_dispatch: concurrency: @@ -10,9 +12,44 @@ concurrency: cancel-in-progress: true jobs: - build: + unit-tests: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-22.04 + + steps: + - name: Check out ${{ github.repository }} + uses: actions/checkout@v6 + + - name: Check out Devolutions/actions + uses: actions/checkout@v6 + with: + repository: Devolutions/actions + ref: v1 + token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }} + path: ./.github/workflows + + - name: Get current version + id: get-version + run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT + + - name: Does tag exists? + uses: ./.github/workflows/tag-check + with: + github_token: ${{ github.token }} + tag: v${{ steps.get-version.outputs.version }} + + - name: Setup Go environment + uses: actions/setup-go@v6 + with: + go-version: ${{ vars.GO_VERSION }} + + - name: Run unit tests + run: go test -v ./... + + integration-tests: environment: test-application + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-22.04 steps: - name: Check out ${{ github.repository }} @@ -59,8 +96,7 @@ jobs: sudo cp ${{ runner.temp }}/certificate.pem /usr/local/share/ca-certificates/certificate.crt sudo update-ca-certificates - - name: Test application - uses: ./.github/workflows/go-test + - name: Run integration tests env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} @@ -70,5 +106,4 @@ jobs: TEST_CERTIFICATE_FILE_PATH: '${{ runner.temp }}/test.p12' TEST_HOST_ENTRY_ID: ${{ secrets.TEST_HOST_ENTRY_ID }} TEST_WEBSITE_ENTRY_ID: ${{ secrets.TEST_WEBSITE_ENTRY_ID }} - with: - github_token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }} + run: go test -tags integration -v ./... diff --git a/dvls_test.go b/dvls_test.go index 85f0d28..298373e 100644 --- a/dvls_test.go +++ b/dvls_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/dvls_unit_test.go b/dvls_unit_test.go new file mode 100644 index 0000000..82ea362 --- /dev/null +++ b/dvls_unit_test.go @@ -0,0 +1,59 @@ +package dvls + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewClient_Login(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/login", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"result":1,"tokenId":"mock-token-123"}`)) + }) + mux.HandleFunc("/api/is-logged", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("true")) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client, err := NewClient("test-key", "test-secret", server.URL) + require.NoError(t, err) + assert.Equal(t, "mock-token-123", client.credential.token) + assert.NotNil(t, client.Entries) + assert.NotNil(t, client.Vaults) +} + +func TestIsLogged_True(t *testing.T) { + client := newTestClient(t, http.NewServeMux()) + + logged, err := client.isLogged() + require.NoError(t, err) + assert.True(t, logged) +} + +func TestIsLogged_False(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/is-logged", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("false")) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client := &Client{ + baseUri: server.URL, + client: server.Client(), + credential: credentials{token: "test-token"}, + } + + logged, err := client.isLogged() + require.NoError(t, err) + assert.False(t, logged) +} diff --git a/entry_certificate_test.go b/entry_certificate_test.go index 4df1115..dbf648e 100644 --- a/entry_certificate_test.go +++ b/entry_certificate_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/entry_credential_test.go b/entry_credential_test.go index b29f508..0a1f1b2 100644 --- a/entry_credential_test.go +++ b/entry_credential_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/entry_credential_unit_test.go b/entry_credential_unit_test.go new file mode 100644 index 0000000..f0f4d99 --- /dev/null +++ b/entry_credential_unit_test.go @@ -0,0 +1,208 @@ +package dvls + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCredentialGetEntries(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"1","name":"Cred1","type":"Credential","subType":"Default","path":"test","data":{"username":"u1"}}, + {"id":"2","name":"Folder1","type":"Folder","subType":"Folder","path":"test","data":{"domain":"d1"}}, + {"id":"3","name":"Cred2","type":"Credential","subType":"ApiKey","path":"test","data":{"apiId":"a1"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 3, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + entries, err := client.Entries.Credential.GetEntries(testVaultID, GetEntriesOptions{}) + require.NoError(t, err) + require.Len(t, entries, 2) + assert.Equal(t, "Cred1", entries[0].Name) + assert.Equal(t, "Cred2", entries[1].Name) +} + +func TestCredentialGetEntries_PathFilter(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"1","name":"InBase","type":"Credential","subType":"Default","path":"base","data":{"username":"u"}}, + {"id":"2","name":"InSub","type":"Credential","subType":"Default","path":"base\\sub","data":{"username":"u"}}, + {"id":"3","name":"InSimilar","type":"Credential","subType":"Default","path":"baseother","data":{"username":"u"}}, + {"id":"4","name":"InRoot","type":"Credential","subType":"Default","path":"","data":{"username":"u"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 4, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + basePath := "base" + entries, err := client.Entries.Credential.GetEntries(testVaultID, GetEntriesOptions{Path: &basePath}) + require.NoError(t, err) + require.Len(t, entries, 2) + + var names []string + for _, e := range entries { + names = append(names, e.Name) + } + assert.Contains(t, names, "InBase") + assert.Contains(t, names, "InSub") + assert.NotContains(t, names, "InSimilar") + assert.NotContains(t, names, "InRoot") +} + +func TestCredentialGetEntries_RootPathFilter(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"1","name":"InBase","type":"Credential","subType":"Default","path":"base","data":{"username":"u"}}, + {"id":"2","name":"InRoot","type":"Credential","subType":"Default","path":"","data":{"username":"u"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 2, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + rootPath := "" + entries, err := client.Entries.Credential.GetEntries(testVaultID, GetEntriesOptions{Path: &rootPath}) + require.NoError(t, err) + require.Len(t, entries, 1) + assert.Equal(t, "InRoot", entries[0].Name) +} + +func TestCredentialGetByName(t *testing.T) { + entryID := "entry-found" + mux := http.NewServeMux() + callCount := 0 + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"entry-found","name":"Target","type":"Credential","subType":"Default","path":"test","data":{"username":"u"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 1, + "pageSize": 20 + }`)) + }) + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry/%s", testVaultID, entryID), func(w http.ResponseWriter, r *http.Request) { + callCount++ + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "id": "entry-found", + "name": "Target", + "type": "Credential", + "subType": "Default", + "path": "test", + "data": {"username": "u", "password": "p"} + }`)) + }) + + client := newTestClient(t, mux) + + testPath := "test" + entry, err := client.Entries.Credential.GetByName(testVaultID, "Target", EntryCredentialSubTypeDefault, GetByNameOptions{Path: &testPath}) + require.NoError(t, err) + assert.Equal(t, "entry-found", entry.Id) + assert.Equal(t, "Target", entry.Name) + assert.Equal(t, 1, callCount) +} + +func TestCredentialGetByName_NotFound(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [], + "currentPage": 1, + "totalPage": 1, + "totalCount": 0, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + _, err := client.Entries.Credential.GetByName(testVaultID, "NonExistent", EntryCredentialSubTypeDefault, GetByNameOptions{}) + assert.ErrorIs(t, err, ErrEntryNotFound) +} + +func TestCredentialGetByName_Multiple(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"1","name":"Dup","type":"Credential","subType":"Default","path":"","data":{"username":"u1"}}, + {"id":"2","name":"Dup","type":"Credential","subType":"Default","path":"","data":{"username":"u2"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 2, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + _, err := client.Entries.Credential.GetByName(testVaultID, "Dup", EntryCredentialSubTypeDefault, GetByNameOptions{}) + assert.ErrorIs(t, err, ErrMultipleEntriesFound) +} + +func TestCredentialValidation_EmptyVaultId(t *testing.T) { + client := newTestClient(t, http.NewServeMux()) + + _, err := client.Entries.Credential.New(Entry{ + Type: EntryCredentialType, + SubType: EntryCredentialSubTypeDefault, + Data: &EntryCredentialDefaultData{}, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "VaultId") +} + +func TestCredentialValidation_UnsupportedSubType(t *testing.T) { + client := newTestClient(t, http.NewServeMux()) + + _, err := client.Entries.Credential.New(Entry{ + VaultId: testVaultID, + Type: EntryCredentialType, + SubType: "InvalidSubType", + Data: &EntryCredentialDefaultData{}, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported") +} diff --git a/entry_folder_test.go b/entry_folder_test.go index 5c2e37c..c5cd8f6 100644 --- a/entry_folder_test.go +++ b/entry_folder_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/entry_folder_unit_test.go b/entry_folder_unit_test.go new file mode 100644 index 0000000..4fe1b49 --- /dev/null +++ b/entry_folder_unit_test.go @@ -0,0 +1,94 @@ +package dvls + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFolderGetEntries(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"1","name":"Folder1","type":"Folder","subType":"Folder","path":"test","data":{"domain":"d1"}}, + {"id":"2","name":"Cred1","type":"Credential","subType":"Default","path":"test","data":{"username":"u1"}}, + {"id":"3","name":"Folder2","type":"Folder","subType":"Database","path":"test","data":{"domain":"d2"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 3, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + entries, err := client.Entries.Folder.GetEntries(testVaultID, GetEntriesOptions{}) + require.NoError(t, err) + require.Len(t, entries, 2) + assert.Equal(t, "Folder1", entries[0].Name) + assert.Equal(t, "Folder2", entries[1].Name) +} + +func TestFolderGetByName(t *testing.T) { + entryID := "folder-found" + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [ + {"id":"folder-found","name":"Target","type":"Folder","subType":"Folder","path":"test","data":{"domain":"d"}} + ], + "currentPage": 1, + "totalPage": 1, + "totalCount": 1, + "pageSize": 20 + }`)) + }) + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry/%s", testVaultID, entryID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "id": "folder-found", + "name": "Target", + "type": "Folder", + "subType": "Folder", + "path": "test", + "data": {"domain": "d", "username": "u"} + }`)) + }) + + client := newTestClient(t, mux) + + entry, err := client.Entries.Folder.GetByName(testVaultID, "Target", GetByNameOptions{}) + require.NoError(t, err) + assert.Equal(t, "folder-found", entry.Id) + assert.Equal(t, "Target", entry.Name) +} + +func TestFolderGetByName_NotFound(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/api/v1/vault/%s/entry", testVaultID), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "result": 1, + "data": [], + "currentPage": 1, + "totalPage": 1, + "totalCount": 0, + "pageSize": 20 + }`)) + }) + + client := newTestClient(t, mux) + + _, err := client.Entries.Folder.GetByName(testVaultID, "NonExistent", GetByNameOptions{}) + assert.ErrorIs(t, err, ErrEntryNotFound) +} diff --git a/entry_host_test.go b/entry_host_test.go index 3aec7cf..fdb6fe8 100644 --- a/entry_host_test.go +++ b/entry_host_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/entry_website_test.go b/entry_website_test.go index 75dd7c1..55ea1a8 100644 --- a/entry_website_test.go +++ b/entry_website_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/helpers_test.go b/helpers_test.go index b670a79..f170edb 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/mock_test.go b/mock_test.go new file mode 100644 index 0000000..c56ef21 --- /dev/null +++ b/mock_test.go @@ -0,0 +1,37 @@ +package dvls + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +const testVaultID = "test-vault-id" + +func newTestClient(t *testing.T, mux *http.ServeMux) *Client { + t.Helper() + + mux.HandleFunc("/api/is-logged", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("true")) + }) + + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := &Client{ + baseUri: server.URL, + client: server.Client(), + credential: credentials{token: "test-token"}, + } + client.common.client = client + client.Entries = &Entries{ + Certificate: (*EntryCertificateService)(&client.common), + Credential: (*EntryCredentialService)(&client.common), + Folder: (*EntryFolderService)(&client.common), + Host: (*EntryHostService)(&client.common), + Website: (*EntryWebsiteService)(&client.common), + } + client.Vaults = (*Vaults)(&client.common) + + return client +} diff --git a/server_test.go b/server_test.go index 212f269..2f048ce 100644 --- a/server_test.go +++ b/server_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/vaults_test.go b/vaults_test.go index 7ba4458..2486488 100644 --- a/vaults_test.go +++ b/vaults_test.go @@ -1,3 +1,5 @@ +//go:build integration + package dvls import ( diff --git a/vaults_unit_test.go b/vaults_unit_test.go new file mode 100644 index 0000000..3b128fc --- /dev/null +++ b/vaults_unit_test.go @@ -0,0 +1,112 @@ +package dvls + +import ( + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVaultsList_Pagination(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/vault", func(w http.ResponseWriter, r *http.Request) { + page := r.URL.Query().Get("page") + w.Header().Set("Content-Type", "application/json") + + switch page { + case "1": + json.NewEncoder(w).Encode(vaultListResponse{ + Data: []Vault{{Id: "vault-1", Name: "Page1Vault"}}, + CurrentPage: 1, + PageSize: 1, + TotalCount: 2, + TotalPage: 2, + }) + case "2": + json.NewEncoder(w).Encode(vaultListResponse{ + Data: []Vault{{Id: "vault-2", Name: "Page2Vault"}}, + CurrentPage: 2, + PageSize: 1, + TotalCount: 2, + TotalPage: 2, + }) + } + }) + + client := newTestClient(t, mux) + + vaults, err := client.Vaults.List() + require.NoError(t, err) + require.Len(t, vaults, 2) + assert.Equal(t, "vault-1", vaults[0].Id) + assert.Equal(t, "vault-2", vaults[1].Id) +} + +func TestVaultsGetByName(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/vault", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(vaultListResponse{ + Data: []Vault{ + {Id: "vault-1", Name: "Alpha"}, + {Id: "vault-2", Name: "Beta"}, + {Id: "vault-3", Name: "Gamma"}, + }, + CurrentPage: 1, + TotalPage: 1, + }) + }) + + client := newTestClient(t, mux) + + vault, err := client.Vaults.GetByName("Beta") + require.NoError(t, err) + assert.Equal(t, "vault-2", vault.Id) + assert.Equal(t, "Beta", vault.Name) +} + +func TestVaultsGetByName_NotFound(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/vault", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(vaultListResponse{ + Data: []Vault{{Id: "vault-1", Name: "Alpha"}}, + CurrentPage: 1, + TotalPage: 1, + }) + }) + + client := newTestClient(t, mux) + + _, err := client.Vaults.GetByName("NonExistent") + assert.ErrorIs(t, err, ErrVaultNotFound) +} + +func TestVaultsNew_DefaultToEverything(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/vault", func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var req vaultRequest + json.Unmarshal(body, &req) + assert.Equal(t, VaultContentTypeEverything, req.ContentType) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(Vault{ + Id: "vault-id", + Name: req.Name, + ContentType: VaultContentTypeEverything, + }) + }) + + client := newTestClient(t, mux) + + created, err := client.Vaults.New(Vault{ + Name: "DefaultVault", + ContentType: VaultContentTypeDefault, + }) + require.NoError(t, err) + assert.Equal(t, VaultContentTypeEverything, created.ContentType) +}