mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-08-11 19:43:43 +08:00
680 lines
17 KiB
Go
680 lines
17 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
)
|
|
|
|
func TestKubernetesService(t *testing.T) {
|
|
log := logger.NewLogger().WithTestConfig()
|
|
log.Init()
|
|
|
|
type testCase struct {
|
|
description string
|
|
run func(t *testing.T, svc *KubernetesService)
|
|
}
|
|
|
|
tests := []testCase{
|
|
{
|
|
description: "Cache by domain returns app and misses unknown domain",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: app,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "foo.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
|
},
|
|
},
|
|
{
|
|
description: "RemoveIngress clears domain and app name entries",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: app,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "foo.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
|
|
|
got = nil
|
|
svc.removeIngress(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
})
|
|
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "foo.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "AddIngressApps replaces stale entries for the same ingress",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
old := model.App{Config: model.AppConfig{Domain: "old.example.com"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: old,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: updated,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "old.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
assert.Nil(t, got)
|
|
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "new.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "new.example.com", got.Config.Domain)
|
|
},
|
|
},
|
|
{
|
|
description: "GetLabels returns app from cache when connected",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
svc.connected = true
|
|
|
|
app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: app,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
var got *model.App
|
|
err := svc.Lookup(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "hit.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "hit.example.com", got.Config.Domain)
|
|
},
|
|
},
|
|
{
|
|
description: "GetLabels returns empty app on cache miss when started",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
svc.connected = true
|
|
|
|
var got *model.App
|
|
err := svc.Lookup(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "notfound.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NoError(t, err)
|
|
require.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "GetLabels resolves app by app name",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
svc.connected = true
|
|
|
|
app := model.App{Path: model.AppPath{Allow: "/foo"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: app,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
var got *model.App
|
|
err := svc.Lookup(func(name string, app *model.App) bool {
|
|
if strings.HasPrefix("foo.internal.example.com", "foo.") {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "/foo", got.Path.Allow)
|
|
},
|
|
},
|
|
{
|
|
description: "GetLabels returns empty app when service not yet started",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
var got *model.App
|
|
err := svc.Lookup(func(name string, app *model.App) bool {
|
|
return false
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem parses annotations and populates cache",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
"tinyauth.apps.myapp.users.allow": "alice",
|
|
})
|
|
item.Object["spec"] = map[string]any{
|
|
"rules": []any{
|
|
map[string]any{
|
|
"host": "myapp.example.com",
|
|
},
|
|
},
|
|
}
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "myapp.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "myapp.example.com", got.Config.Domain)
|
|
assert.Equal(t, "alice", got.Users.Allow)
|
|
},
|
|
},
|
|
{
|
|
description: "Update from item skips annotations with no hosts",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
})
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "myapp.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem fails when label parsing fails",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
"tinyauth.apps.myapp.users.break": "i-dont-exist",
|
|
})
|
|
item.Object["spec"] = map[string]any{
|
|
"rules": []any{
|
|
map[string]any{
|
|
"host": "myapp.example.com",
|
|
},
|
|
},
|
|
}
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "myapp.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
require.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem with no annotations removes existing cache entries",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}}
|
|
svc.addIngressEntries(ingressKey{
|
|
namespace: "default",
|
|
name: "my-ingress",
|
|
}, []ingressEntry{
|
|
{
|
|
app: app,
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("my-ingress")
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if app.Config.Domain == "todelete.example.com" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractPaths returns all non empty paths from a rule",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
rule := map[string]any{
|
|
"http": map[string]any{
|
|
"paths": []any{
|
|
map[string]any{"path": "/"},
|
|
map[string]any{"path": "/api"},
|
|
map[string]any{"path": ""},
|
|
map[string]any{"pathType": "Prefix"},
|
|
"not-a-map",
|
|
},
|
|
},
|
|
}
|
|
|
|
paths, err := svc.extractPaths(rule)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"/", "/api"}, paths)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractPaths returns nothing when http or paths are missing",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
paths, err := svc.extractPaths(map[string]any{})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, paths)
|
|
|
|
paths, err = svc.extractPaths(map[string]any{
|
|
"http": map[string]any{},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, paths)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractPaths errors when http is not a map",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
paths, err := svc.extractPaths(map[string]any{
|
|
"http": "invalid",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Nil(t, paths)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractPaths errors when paths is not a slice",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
paths, err := svc.extractPaths(map[string]any{
|
|
"http": map[string]any{
|
|
"paths": "invalid",
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
assert.Nil(t, paths)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractHosts returns hosts from all rules",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "foo.example.com",
|
|
"http": map[string]any{
|
|
"paths": []any{
|
|
map[string]any{"path": "/"},
|
|
},
|
|
},
|
|
},
|
|
map[string]any{
|
|
"host": "bar.example.com",
|
|
},
|
|
map[string]any{
|
|
"host": "",
|
|
},
|
|
"not-a-map",
|
|
}, "spec", "rules"))
|
|
|
|
hosts, err := svc.extractHosts(&item)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"foo.example.com", "bar.example.com"}, hosts)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractHosts still returns hosts when a rule has no catch all path",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "foo.example.com",
|
|
"http": map[string]any{
|
|
"paths": []any{
|
|
map[string]any{"path": "/api"},
|
|
},
|
|
},
|
|
},
|
|
}, "spec", "rules"))
|
|
|
|
hosts, err := svc.extractHosts(&item)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"foo.example.com"}, hosts)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractHosts still returns hosts when path extraction fails",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "foo.example.com",
|
|
"http": "invalid",
|
|
},
|
|
}, "spec", "rules"))
|
|
|
|
hosts, err := svc.extractHosts(&item)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"foo.example.com"}, hosts)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractHosts returns nothing when spec.rules is missing",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
|
|
hosts, err := svc.extractHosts(&item)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, hosts)
|
|
},
|
|
},
|
|
{
|
|
description: "ExtractHosts errors when spec.rules is not a slice",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules"))
|
|
|
|
hosts, err := svc.extractHosts(&item)
|
|
require.Error(t, err)
|
|
assert.Nil(t, hosts)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem registers app when its domain matches an ingress host",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
})
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "myapp.example.com",
|
|
},
|
|
}, "spec", "rules"))
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if name == "myapp" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "myapp.example.com", got.Config.Domain)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem registers app when its name matches an ingress host prefix",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.users.allow": "alice",
|
|
})
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "MyApp.example.com",
|
|
},
|
|
}, "spec", "rules"))
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if name == "myapp" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "alice", got.Users.Allow)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem skips apps that match neither host nor name",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
})
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "other.example.com",
|
|
},
|
|
}, "spec", "rules"))
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
got = app
|
|
return true
|
|
})
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem falls back to app name when the domain is invalid",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace("default")
|
|
item.SetName("test-ingress")
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "not a domain",
|
|
})
|
|
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
|
|
map[string]any{
|
|
"host": "myapp.example.com",
|
|
},
|
|
}, "spec", "rules"))
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
if name == "myapp" {
|
|
got = app
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
require.NotNil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem removes entries when host extraction fails",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
key := ingressKey{
|
|
namespace: "default",
|
|
name: "test-ingress",
|
|
}
|
|
svc.addIngressEntries(key, []ingressEntry{
|
|
{
|
|
app: model.App{Config: model.AppConfig{Domain: "stale.example.com"}},
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace(key.namespace)
|
|
item.SetName(key.name)
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
})
|
|
require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules"))
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
got = app
|
|
return true
|
|
})
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
{
|
|
description: "UpdateFromItem removes entries when annotations are not decodable",
|
|
run: func(t *testing.T, svc *KubernetesService) {
|
|
key := ingressKey{
|
|
namespace: "default",
|
|
name: "test-ingress",
|
|
}
|
|
svc.addIngressEntries(key, []ingressEntry{
|
|
{
|
|
app: model.App{Config: model.AppConfig{Domain: "stale.example.com"}},
|
|
name: "foo",
|
|
},
|
|
})
|
|
|
|
item := unstructured.Unstructured{}
|
|
item.SetNamespace(key.namespace)
|
|
item.SetName(key.name)
|
|
item.SetAnnotations(map[string]string{
|
|
"tinyauth.apps.myapp.config.oauthWhitelist": "[",
|
|
})
|
|
|
|
svc.updateFromItem(&item)
|
|
|
|
var got *model.App
|
|
svc.getEntry(func(name string, app *model.App) bool {
|
|
got = app
|
|
return true
|
|
})
|
|
assert.Nil(t, got)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
svc := &KubernetesService{
|
|
ingressEntries: make(map[ingressKey][]ingressEntry),
|
|
log: log,
|
|
}
|
|
test.run(t, svc)
|
|
})
|
|
}
|
|
}
|