fix more test scenarios

This commit is contained in:
Jan Husak
2025-11-19 12:40:54 +01:00
parent 41a1f70d02
commit 26d072c8ec
3 changed files with 194 additions and 52 deletions

View File

@@ -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)...)
} }

View File

@@ -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()
}

View File

@@ -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
}
}