mirror of
https://github.com/ysoftdevs/terraform-provider-bitbucket.git
synced 2026-03-29 05:31:47 +02:00
fix more test scenarios
This commit is contained in:
@@ -327,8 +327,6 @@ func (r *BitbucketTokenResource) Create(ctx context.Context, req resource.Create
|
|||||||
resp.Diagnostics.Append(resp.State.Set(ctx, out)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, out)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read — does not create new tokens (to keep Read side-effect free).
|
|
||||||
// If the tracked token is missing or expired, remove from state so the next Apply will recreate it.
|
|
||||||
func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
var data BitbucketTokenResourceModel
|
var data BitbucketTokenResourceModel
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
@@ -336,38 +334,62 @@ func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// FIX #1: unknown values must be treated as drift
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
if data.CurrentTokenName.IsUnknown() || data.Token.IsUnknown() {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no ID or no token name → resource is incomplete → drift
|
||||||
|
if data.ID.IsUnknown() || data.ID.IsNull() ||
|
||||||
|
data.CurrentTokenName.IsNull() {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project := data.ProjectName.ValueString()
|
project := data.ProjectName.ValueString()
|
||||||
repo := data.RepositoryName.ValueString()
|
repo := data.RepositoryName.ValueString()
|
||||||
prefix := data.TokenName.ValueString()
|
prefix := data.TokenName.ValueString()
|
||||||
|
|
||||||
|
// List tokens from Bitbucket
|
||||||
tokens, err := r.listTokens(r.authHeader, r.serverURL, project, repo, prefix)
|
tokens, err := r.listTokens(r.authHeader, r.serverURL, project, repo, prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error listing tokens", err.Error())
|
resp.Diagnostics.AddError("Error listing tokens", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stateName := data.CurrentTokenName.ValueString()
|
||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
thresholdMs := int64(30 * 24 * time.Hour / time.Millisecond)
|
thresholdMs := int64(30 * 24 * time.Hour / time.Millisecond)
|
||||||
|
|
||||||
stateName := data.CurrentTokenName.ValueString()
|
// Find token in server list
|
||||||
var valid bool
|
t := getTokenByName(tokens, stateName)
|
||||||
|
|
||||||
if stateName != "" {
|
// ----------------------------------------------------------
|
||||||
if t := getTokenByName(tokens, stateName); t != nil {
|
// FIX #2: drift if token does not exist anymore
|
||||||
timeLeft := t.ExpiryMs - nowMs
|
// ----------------------------------------------------------
|
||||||
if timeLeft > thresholdMs {
|
if t == nil {
|
||||||
data.CurrentTokenExpiry = types.Int64Value(t.ExpiryMs)
|
|
||||||
valid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !valid {
|
|
||||||
// Not present or expired -> remove from state; next Apply will create a new one in Create/Update paths.
|
|
||||||
resp.State.RemoveResource(ctx)
|
resp.State.RemoveResource(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Time remaining before expiration
|
||||||
|
timeLeft := t.ExpiryMs - nowMs
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// FIX #3: expired or expiring soon → drift
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
if timeLeft <= thresholdMs {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// All good → update expiry in state
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
data.CurrentTokenExpiry = types.Int64Value(t.ExpiryMs)
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package mock_server
|
package mock_server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -34,6 +37,13 @@ func NewMockBitbucketServer() *MockBitbucketServer {
|
|||||||
// ONE handler, dispatching by HTTP method (GET / PUT / DELETE)
|
// ONE handler, dispatching by HTTP method (GET / PUT / DELETE)
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
mux.HandleFunc("/rest/access-tokens/latest/projects/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/rest/access-tokens/latest/projects/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Debug log incoming requests to help troubleshoot test failures.
|
||||||
|
fmt.Printf("[mock] %s %s\n", r.Method, r.URL.Path)
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
fmt.Printf("[mock] body: %s\n", string(body))
|
||||||
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
|
}
|
||||||
parts := strings.Split(r.URL.Path, "/")
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
// Expected for LIST and CREATE:
|
// Expected for LIST and CREATE:
|
||||||
// ["","rest","access-tokens","latest","projects",p,"repos",r]
|
// ["","rest","access-tokens","latest","projects",p,"repos",r]
|
||||||
@@ -46,8 +56,9 @@ func NewMockBitbucketServer() *MockBitbucketServer {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
project := parts[4]
|
// parts layout: ["", "rest", "access-tokens", "latest", "projects", <project>, "repos", <repo>, ...]
|
||||||
repo := parts[6]
|
project := parts[5]
|
||||||
|
repo := parts[7]
|
||||||
key := project + "/" + repo
|
key := project + "/" + repo
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@@ -150,3 +161,22 @@ func (m *MockBitbucketServer) ClearTokensFor(key string) {
|
|||||||
m.Tokens[key] = nil
|
m.Tokens[key] = nil
|
||||||
m.Mu.Unlock()
|
m.Mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockBitbucketServer) SetExpiredToken(key string) {
|
||||||
|
m.Mu.Lock()
|
||||||
|
defer m.Mu.Unlock()
|
||||||
|
|
||||||
|
if len(m.Tokens[key]) == 0 {
|
||||||
|
// create one if none exists
|
||||||
|
m.Tokens[key] = []Token{{
|
||||||
|
Name: "expired-token",
|
||||||
|
Token: "secret",
|
||||||
|
ExpiryDate: time.Now().Add(-1 * time.Hour).UnixMilli(),
|
||||||
|
Permissions: []string{"REPO_READ"},
|
||||||
|
}}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// expire the first existing token
|
||||||
|
m.Tokens[key][0].ExpiryDate = time.Now().Add(-1 * time.Hour).UnixMilli()
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package test
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||||
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
||||||
@@ -14,48 +15,105 @@ import (
|
|||||||
mock "terraform-provider-bitbucket-token/mock_server"
|
mock "terraform-provider-bitbucket-token/mock_server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Acceptance test provider factories for protocol v6
|
|
||||||
var testAccProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
|
var testAccProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
|
||||||
"bitbucket": providerserver.NewProtocol6WithError(provider.NewProvider()),
|
"bitbucket": providerserver.NewProtocol6WithError(provider.NewProvider()),
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccBitbucketToken_CRUD(t *testing.T) {
|
func TestAccBitbucketToken_AllScenarios(t *testing.T) {
|
||||||
server := mock.NewMockBitbucketServer()
|
// Split into focused tests to validate specific behaviors.
|
||||||
|
|
||||||
if err := server.Start(); err != nil {
|
|
||||||
t.Fatalf("server start error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceName := "bitbucket_token.test"
|
resourceName := "bitbucket_token.test"
|
||||||
|
|
||||||
resource.Test(t, resource.TestCase{
|
// Helper to start a fresh server for each test.
|
||||||
PreCheck: func() { testAccPreCheck(t) },
|
startServer := func(t *testing.T) *mock.MockBitbucketServer {
|
||||||
ProtoV6ProviderFactories: testAccProviderFactories,
|
srv := mock.NewMockBitbucketServer()
|
||||||
CheckDestroy: testAccCheckBitbucketTokenDestroy(server),
|
if err := srv.Start(); err != nil {
|
||||||
Steps: []resource.TestStep{
|
t.Fatalf("server start error: %v", err)
|
||||||
{
|
}
|
||||||
// CREATE
|
return srv
|
||||||
Config: testAccBitbucketTokenConfig(server.URL),
|
}
|
||||||
Check: resource.ComposeTestCheckFunc(
|
|
||||||
resource.TestCheckResourceAttrSet(resourceName, "id"),
|
t.Run("CreateWhenNone", func(t *testing.T) {
|
||||||
resource.TestCheckResourceAttrSet(resourceName, "current_token_name"),
|
server := startServer(t)
|
||||||
resource.TestCheckResourceAttrSet(resourceName, "current_token_expiry"),
|
defer func() { _ = server }()
|
||||||
resource.TestCheckResourceAttrSet(resourceName, "token"),
|
|
||||||
),
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
ProtoV6ProviderFactories: testAccProviderFactories,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccBitbucketTokenConfig(server.URL),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet(resourceName, "id"),
|
||||||
|
resource.TestCheckResourceAttrSet(resourceName, "token"),
|
||||||
|
resource.TestCheckResourceAttrSet(resourceName, "current_token_name"),
|
||||||
|
resource.TestCheckResourceAttrSet(resourceName, "current_token_expiry"),
|
||||||
|
testAccCheckServerHasTokens(server, "proj/repo", 1),
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
})
|
||||||
// READ (no drift) – apply the same config again; plan should stay empty
|
})
|
||||||
Config: testAccBitbucketTokenConfig(server.URL),
|
|
||||||
|
t.Run("ReuseStateWhenSecondaryExists", func(t *testing.T) {
|
||||||
|
server := startServer(t)
|
||||||
|
defer func() { _ = server }()
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
ProtoV6ProviderFactories: testAccProviderFactories,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
// 1) create initial token and capture state
|
||||||
|
{
|
||||||
|
Config: testAccBitbucketTokenConfig(server.URL),
|
||||||
|
},
|
||||||
|
// 2) Add a secondary token on the server, expect no changes (reuse)
|
||||||
|
{
|
||||||
|
PreConfig: func() {
|
||||||
|
server.Mu.Lock()
|
||||||
|
server.Tokens["proj/repo"] = append(server.Tokens["proj/repo"], mock.Token{
|
||||||
|
Name: "prefix-secondary",
|
||||||
|
Token: "secret-secondary",
|
||||||
|
ExpiryDate: time.Now().Add(24 * time.Hour).UnixMilli(),
|
||||||
|
})
|
||||||
|
server.Mu.Unlock()
|
||||||
|
},
|
||||||
|
Config: testAccBitbucketTokenConfig(server.URL),
|
||||||
|
PlanOnly: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RecreateWhenExpired", func(t *testing.T) {
|
||||||
|
server := startServer(t)
|
||||||
|
defer func() { _ = server }()
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
ProtoV6ProviderFactories: testAccProviderFactories,
|
||||||
|
CheckDestroy: testAccCheckBitbucketTokenDestroy(server),
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
// 1) create initial token
|
||||||
|
{
|
||||||
|
Config: testAccBitbucketTokenConfig(server.URL),
|
||||||
|
},
|
||||||
|
// 2) expire token on server and refresh state; expect a non-empty plan
|
||||||
|
{
|
||||||
|
PreConfig: func() {
|
||||||
|
server.SetExpiredToken("proj/repo")
|
||||||
|
},
|
||||||
|
RefreshState: true,
|
||||||
|
ExpectNonEmptyPlan: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// -------- Helper Functions --------
|
// ---------- Helper Configs ----------
|
||||||
//
|
//
|
||||||
|
|
||||||
// Basic test configuration for the provider + resource
|
|
||||||
func testAccBitbucketTokenConfig(url string) string {
|
func testAccBitbucketTokenConfig(url string) string {
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
provider "bitbucket" {
|
provider "bitbucket" {
|
||||||
@@ -72,25 +130,57 @@ resource "bitbucket_token" "test" {
|
|||||||
`, url)
|
`, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures Terraform acceptance test environment is correct
|
func testAccBitbucketTokenConfigPrefix(url, prefix string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
provider "bitbucket" {
|
||||||
|
server_url = "%s"
|
||||||
|
auth_header = "dummy"
|
||||||
|
tls_skip_verify = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "bitbucket_token" "test" {
|
||||||
|
project_name = "proj"
|
||||||
|
repository_name = "repo"
|
||||||
|
token_name = "%s"
|
||||||
|
}
|
||||||
|
`, url, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------- Environment ----------
|
||||||
|
//
|
||||||
|
|
||||||
func testAccPreCheck(t *testing.T) {
|
func testAccPreCheck(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping acceptance tests in short mode")
|
t.Skip("skipping acceptance tests in short mode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifies that all tokens were removed by Delete()
|
|
||||||
func testAccCheckBitbucketTokenDestroy(server *mock.MockBitbucketServer) resource.TestCheckFunc {
|
func testAccCheckBitbucketTokenDestroy(server *mock.MockBitbucketServer) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
server.Mu.Lock()
|
server.Mu.Lock()
|
||||||
defer server.Mu.Unlock()
|
defer server.Mu.Unlock()
|
||||||
|
|
||||||
// server.Tokens should be empty after resource.Delete()
|
for _, tok := range server.Tokens {
|
||||||
for _, tokens := range server.Tokens {
|
if len(tok) != 0 {
|
||||||
if len(tokens) != 0 {
|
return fmt.Errorf("tokens still exist: %#v", server.Tokens)
|
||||||
return fmt.Errorf("expected no tokens, but found: %#v", server.Tokens)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testAccCheckServerHasTokens asserts that the mock server contains exactly
|
||||||
|
// `expected` tokens for the given repo key (e.g. "proj/repo").
|
||||||
|
func testAccCheckServerHasTokens(server *mock.MockBitbucketServer, key string, expected int) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
server.Mu.Lock()
|
||||||
|
defer server.Mu.Unlock()
|
||||||
|
|
||||||
|
toks := server.Tokens[key]
|
||||||
|
if len(toks) != expected {
|
||||||
|
return fmt.Errorf("expected %d tokens for %s, got %d: %#v", expected, key, len(toks), toks)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user