2025-10-20 13:34:22 +02:00
// ABOUTME: Unit tests for AppInstance management APIs using httptest mock server
// ABOUTME: Tests create, show, list, refresh, and delete operations with error conditions
package v2
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateAppInstance ( t * testing . T ) {
tests := [ ] struct {
name string
input * NewAppInstanceInput
mockStatusCode int
mockResponse string
expectError bool
errorContains string
} {
{
name : "successful creation" ,
input : & NewAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
AppKey : AppKey {
Organization : "testorg" ,
Name : "testapp" ,
Version : "1.0.0" ,
} ,
Flavor : Flavor { Name : "m4.small" } ,
} ,
} ,
mockStatusCode : 200 ,
mockResponse : ` { "message": "success"} ` ,
expectError : false ,
} ,
{
name : "validation error" ,
input : & NewAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "" ,
Name : "testinst" ,
} ,
} ,
} ,
mockStatusCode : 400 ,
mockResponse : ` { "message": "organization is required"} ` ,
expectError : true ,
} ,
{
name : "HTTP 200 with CreateError message" ,
input : & NewAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
Flavor : Flavor { Name : "m4.small" } ,
} ,
} ,
mockStatusCode : 200 ,
mockResponse : ` { "data" : { "message" : "Creating" } }
{ "data" : { "message" : "a service has been configured" } }
{ "data" : { "message" : "CreateError" } }
{ "data" : { "message" : "Deleting AppInst due to failure" } }
{ "data" : { "message" : "Deleted AppInst successfully" } } ` ,
expectError : true ,
errorContains : "CreateError" ,
} ,
{
name : "HTTP 200 with result error code" ,
input : & NewAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
Flavor : Flavor { Name : "m4.small" } ,
} ,
} ,
mockStatusCode : 200 ,
mockResponse : ` { "data" : { "message" : "Creating" } }
{ "data" : { "message" : "a service has been configured" } }
{ "data" : { "message" : "CreateError" } }
{ "data" : { "message" : "Deleting AppInst due to failure" } }
{ "data" : { "message" : "Deleted AppInst successfully" } }
{ "result" : { "message" : "Encountered failures: Create App Inst failed: deployments.apps is forbidden: User \"system:serviceaccount:edgexr:crm-telekomop-munich\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"gitea\"" , "code" : 400 } } ` ,
expectError : true ,
errorContains : "deployments.apps is forbidden" ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
// Create mock server
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "POST" , r . Method )
assert . Equal ( t , "/api/v1/auth/ctrl/CreateAppInst" , r . URL . Path )
assert . Equal ( t , "application/json" , r . Header . Get ( "Content-Type" ) )
w . WriteHeader ( tt . mockStatusCode )
2025-10-22 12:47:15 +02:00
_ , _ = w . Write ( [ ] byte ( tt . mockResponse ) )
2025-10-20 13:34:22 +02:00
} ) )
defer server . Close ( )
// Create client
client := NewClient ( server . URL ,
WithHTTPClient ( & http . Client { Timeout : 5 * time . Second } ) ,
WithAuthProvider ( NewStaticTokenProvider ( "test-token" ) ) ,
)
// Execute test
ctx := context . Background ( )
err := client . CreateAppInstance ( ctx , tt . input )
// Verify results
if tt . expectError {
assert . Error ( t , err )
if tt . errorContains != "" {
assert . Contains ( t , err . Error ( ) , tt . errorContains )
}
} else {
assert . NoError ( t , err )
}
} )
}
}
func TestShowAppInstance ( t * testing . T ) {
tests := [ ] struct {
name string
appInstKey AppInstanceKey
2025-11-13 15:40:36 +01:00
appKey AppKey
2025-10-20 13:34:22 +02:00
region string
mockStatusCode int
mockResponse string
expectError bool
expectNotFound bool
} {
{
name : "successful show" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
2025-11-13 15:40:36 +01:00
appKey : AppKey { Name : "testapp" } ,
2025-10-20 13:34:22 +02:00
region : "us-west" ,
mockStatusCode : 200 ,
mockResponse : ` { "data" : { "key" : { "organization" : "testorg" , "name" : "testinst" , "cloudlet_key" : { "organization" : "cloudletorg" , "name" : "testcloudlet" } } , "state" : "Ready" } }
` ,
expectError : false ,
expectNotFound : false ,
} ,
{
name : "instance not found" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "nonexistent" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
2025-11-13 15:40:36 +01:00
appKey : AppKey { Name : "testapp" } ,
2025-10-20 13:34:22 +02:00
region : "us-west" ,
mockStatusCode : 404 ,
mockResponse : "" ,
expectError : true ,
expectNotFound : true ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
// Create mock server
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "POST" , r . Method )
assert . Equal ( t , "/api/v1/auth/ctrl/ShowAppInst" , r . URL . Path )
w . WriteHeader ( tt . mockStatusCode )
if tt . mockResponse != "" {
2025-10-22 12:47:15 +02:00
_ , _ = w . Write ( [ ] byte ( tt . mockResponse ) )
2025-10-20 13:34:22 +02:00
}
} ) )
defer server . Close ( )
// Create client
client := NewClient ( server . URL ,
WithHTTPClient ( & http . Client { Timeout : 5 * time . Second } ) ,
)
// Execute test
ctx := context . Background ( )
2025-11-13 15:40:36 +01:00
appInst , err := client . ShowAppInstance ( ctx , tt . appInstKey , tt . appKey , tt . region )
2025-10-20 13:34:22 +02:00
// Verify results
if tt . expectError {
assert . Error ( t , err )
if tt . expectNotFound {
assert . Contains ( t , err . Error ( ) , "resource not found" )
}
} else {
require . NoError ( t , err )
assert . Equal ( t , tt . appInstKey . Organization , appInst . Key . Organization )
assert . Equal ( t , tt . appInstKey . Name , appInst . Key . Name )
assert . Equal ( t , "Ready" , appInst . State )
}
} )
}
}
func TestShowAppInstances ( t * testing . T ) {
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "POST" , r . Method )
assert . Equal ( t , "/api/v1/auth/ctrl/ShowAppInst" , r . URL . Path )
// Verify request body
var filter AppInstanceFilter
err := json . NewDecoder ( r . Body ) . Decode ( & filter )
require . NoError ( t , err )
assert . Equal ( t , "testorg" , filter . AppInstance . Key . Organization )
assert . Equal ( t , "us-west" , filter . Region )
// Return multiple app instances
response := ` { "data" : { "key" : { "organization" : "testorg" , "name" : "inst1" } , "state" : "Ready" } }
{ "data" : { "key" : { "organization" : "testorg" , "name" : "inst2" } , "state" : "Creating" } }
`
w . WriteHeader ( 200 )
2025-10-22 12:47:15 +02:00
_ , _ = w . Write ( [ ] byte ( response ) )
2025-10-20 13:34:22 +02:00
} ) )
defer server . Close ( )
client := NewClient ( server . URL )
ctx := context . Background ( )
2025-11-13 16:59:38 +01:00
appInstances , err := client . ShowAppInstances ( ctx , AppInstanceKey { Organization : "testorg" } , AppKey { } , "us-west" )
2025-10-20 13:34:22 +02:00
require . NoError ( t , err )
assert . Len ( t , appInstances , 2 )
assert . Equal ( t , "inst1" , appInstances [ 0 ] . Key . Name )
assert . Equal ( t , "Ready" , appInstances [ 0 ] . State )
assert . Equal ( t , "inst2" , appInstances [ 1 ] . Key . Name )
assert . Equal ( t , "Creating" , appInstances [ 1 ] . State )
}
func TestUpdateAppInstance ( t * testing . T ) {
tests := [ ] struct {
name string
input * UpdateAppInstanceInput
mockStatusCode int
mockResponse string
expectError bool
} {
{
name : "successful update" ,
input : & UpdateAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
AppKey : AppKey {
Organization : "testorg" ,
Name : "testapp" ,
Version : "1.0.0" ,
} ,
Flavor : Flavor { Name : "m4.medium" } ,
PowerState : "PowerOn" ,
} ,
} ,
mockStatusCode : 200 ,
mockResponse : ` { "message": "success"} ` ,
expectError : false ,
} ,
{
name : "validation error" ,
input : & UpdateAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
} ,
} ,
mockStatusCode : 400 ,
mockResponse : ` { "message": "organization is required"} ` ,
expectError : true ,
} ,
{
name : "instance not found" ,
input : & UpdateAppInstanceInput {
Region : "us-west" ,
AppInst : AppInstance {
Key : AppInstanceKey {
Organization : "testorg" ,
Name : "nonexistent" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
} ,
} ,
mockStatusCode : 404 ,
mockResponse : ` { "message": "app instance not found"} ` ,
expectError : true ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
// Create mock server
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "POST" , r . Method )
assert . Equal ( t , "/api/v1/auth/ctrl/UpdateAppInst" , r . URL . Path )
assert . Equal ( t , "application/json" , r . Header . Get ( "Content-Type" ) )
// Verify request body
var input UpdateAppInstanceInput
err := json . NewDecoder ( r . Body ) . Decode ( & input )
require . NoError ( t , err )
assert . Equal ( t , tt . input . Region , input . Region )
assert . Equal ( t , tt . input . AppInst . Key . Organization , input . AppInst . Key . Organization )
w . WriteHeader ( tt . mockStatusCode )
2025-10-22 12:47:15 +02:00
_ , _ = w . Write ( [ ] byte ( tt . mockResponse ) )
2025-10-20 13:34:22 +02:00
} ) )
defer server . Close ( )
// Create client
client := NewClient ( server . URL ,
WithHTTPClient ( & http . Client { Timeout : 5 * time . Second } ) ,
WithAuthProvider ( NewStaticTokenProvider ( "test-token" ) ) ,
)
// Execute test
ctx := context . Background ( )
err := client . UpdateAppInstance ( ctx , tt . input )
// Verify results
if tt . expectError {
assert . Error ( t , err )
} else {
assert . NoError ( t , err )
}
} )
}
}
func TestRefreshAppInstance ( t * testing . T ) {
tests := [ ] struct {
name string
appInstKey AppInstanceKey
region string
mockStatusCode int
expectError bool
} {
{
name : "successful refresh" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
region : "us-west" ,
mockStatusCode : 200 ,
expectError : false ,
} ,
{
name : "server error" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
region : "us-west" ,
mockStatusCode : 500 ,
expectError : true ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "POST" , r . Method )
assert . Equal ( t , "/api/v1/auth/ctrl/RefreshAppInst" , r . URL . Path )
w . WriteHeader ( tt . mockStatusCode )
} ) )
defer server . Close ( )
client := NewClient ( server . URL )
ctx := context . Background ( )
err := client . RefreshAppInstance ( ctx , tt . appInstKey , tt . region )
if tt . expectError {
assert . Error ( t , err )
} else {
assert . NoError ( t , err )
}
} )
}
}
func TestDeleteAppInstance ( t * testing . T ) {
tests := [ ] struct {
name string
appInstKey AppInstanceKey
region string
mockStatusCode int
expectError bool
} {
{
name : "successful deletion" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
region : "us-west" ,
mockStatusCode : 200 ,
expectError : false ,
} ,
{
name : "already deleted (404 ok)" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
region : "us-west" ,
mockStatusCode : 404 ,
expectError : false ,
} ,
{
name : "server error" ,
appInstKey : AppInstanceKey {
Organization : "testorg" ,
Name : "testinst" ,
CloudletKey : CloudletKey {
Organization : "cloudletorg" ,
Name : "testcloudlet" ,
} ,
} ,
region : "us-west" ,
mockStatusCode : 500 ,
expectError : true ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "POST" , r . Method )
assert . Equal ( t , "/api/v1/auth/ctrl/DeleteAppInst" , r . URL . Path )
w . WriteHeader ( tt . mockStatusCode )
} ) )
defer server . Close ( )
client := NewClient ( server . URL )
ctx := context . Background ( )
err := client . DeleteAppInstance ( ctx , tt . appInstKey , tt . region )
if tt . expectError {
assert . Error ( t , err )
} else {
assert . NoError ( t , err )
}
} )
}
}