454 lines
14 KiB
Go
454 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"helix-proxy/internal/store"
|
|
)
|
|
|
|
func apiPutRaw(t *testing.T, url, rawBody, token string) *http.Response {
|
|
t.Helper()
|
|
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader([]byte(rawBody)))
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("do: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func TestAPI_UnauthorizedEndpoints(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
paths := []string{
|
|
"/api/proxy-hosts",
|
|
"/api/access-lists",
|
|
"/api/streams",
|
|
"/api/certificates",
|
|
"/api/redirection-hosts",
|
|
"/api/dead-hosts",
|
|
"/api/settings",
|
|
"/api/audit-logs",
|
|
"/api/dashboard",
|
|
"/api/users",
|
|
"/api/users/me",
|
|
}
|
|
for _, path := range paths {
|
|
t.Run(path, func(t *testing.T) {
|
|
assertStatus(t, apiGet(t, env.BaseURL+path, ""), http.StatusUnauthorized)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPI_BadIDBranches(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
want int
|
|
}{
|
|
{"proxy-host get", http.MethodGet, "/api/proxy-hosts/nope", http.StatusBadRequest},
|
|
{"proxy-host put", http.MethodPut, "/api/proxy-hosts/nope", http.StatusBadRequest},
|
|
{"proxy-host delete", http.MethodDelete, "/api/proxy-hosts/bad", http.StatusBadRequest},
|
|
{"proxy-host enable", http.MethodPost, "/api/proxy-hosts/x/enable", http.StatusBadRequest},
|
|
{"proxy-host disable", http.MethodPost, "/api/proxy-hosts/x/disable", http.StatusBadRequest},
|
|
|
|
{"access-list put", http.MethodPut, "/api/access-lists/abc", http.StatusBadRequest},
|
|
{"access-list delete", http.MethodDelete, "/api/access-lists/abc", http.StatusBadRequest},
|
|
|
|
{"stream put", http.MethodPut, "/api/streams/nope", http.StatusBadRequest},
|
|
{"stream delete", http.MethodDelete, "/api/streams/bad", http.StatusBadRequest},
|
|
{"stream enable", http.MethodPost, "/api/streams/x/enable", http.StatusBadRequest},
|
|
{"stream disable", http.MethodPost, "/api/streams/x/disable", http.StatusBadRequest},
|
|
|
|
{"cert delete", http.MethodDelete, "/api/certificates/bad", http.StatusBadRequest},
|
|
|
|
{"redir put", http.MethodPut, "/api/redirection-hosts/nope", http.StatusBadRequest},
|
|
{"redir delete", http.MethodDelete, "/api/redirection-hosts/bad", http.StatusBadRequest},
|
|
{"redir disable", http.MethodPost, "/api/redirection-hosts/x/disable", http.StatusBadRequest},
|
|
|
|
{"dead put", http.MethodPut, "/api/dead-hosts/nope", http.StatusBadRequest},
|
|
{"dead delete", http.MethodDelete, "/api/dead-hosts/bad", http.StatusBadRequest},
|
|
{"dead disable", http.MethodPost, "/api/dead-hosts/x/disable", http.StatusBadRequest},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var resp *http.Response
|
|
switch tc.method {
|
|
case http.MethodGet:
|
|
resp = apiGet(t, env.BaseURL+tc.path, token)
|
|
case http.MethodPut:
|
|
resp = apiPut(t, env.BaseURL+tc.path, map[string]any{"name": "x"}, token)
|
|
case http.MethodDelete:
|
|
resp = apiDelete(t, env.BaseURL+tc.path, token)
|
|
default:
|
|
resp = apiPost(t, env.BaseURL+tc.path, nil, token)
|
|
}
|
|
assertStatus(t, resp, tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPI_NotFoundEnableDisable(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
cases := []string{
|
|
"/api/proxy-hosts/9999/disable",
|
|
"/api/streams/9999/enable",
|
|
"/api/streams/9999/disable",
|
|
"/api/redirection-hosts/9999/enable",
|
|
"/api/dead-hosts/9999/disable",
|
|
"/api/dead-hosts/9999/enable",
|
|
}
|
|
for _, path := range cases {
|
|
t.Run(path, func(t *testing.T) {
|
|
assertStatus(t, apiPost(t, env.BaseURL+path, nil, token), http.StatusNotFound)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPI_MalformedJSONOnCreateAndUpdate(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
createPaths := []string{
|
|
"/api/proxy-hosts",
|
|
"/api/access-lists",
|
|
"/api/streams",
|
|
"/api/redirection-hosts",
|
|
"/api/dead-hosts",
|
|
}
|
|
for _, path := range createPaths {
|
|
t.Run("post "+path, func(t *testing.T) {
|
|
assertStatus(t, apiPostRaw(t, env.BaseURL+path, `{`, token), http.StatusBadRequest)
|
|
})
|
|
}
|
|
|
|
ph := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
|
|
"domainNames": []string{"malformed-ph.example"},
|
|
"forwardHost": "127.0.0.1",
|
|
"forwardPort": 1,
|
|
"forwardScheme": "http",
|
|
}, token)
|
|
if ph.StatusCode != http.StatusCreated {
|
|
t.Fatalf("proxy host: %d", ph.StatusCode)
|
|
}
|
|
var proxyHost store.ProxyHost
|
|
decodeJSON(t, ph, &proxyHost)
|
|
|
|
al := apiPost(t, env.BaseURL+"/api/access-lists", map[string]any{
|
|
"name": "malformed-acl",
|
|
"clients": []map[string]string{{"address": "all", "directive": "allow"}},
|
|
}, token)
|
|
if al.StatusCode != http.StatusCreated {
|
|
t.Fatalf("acl: %d", al.StatusCode)
|
|
}
|
|
var accessList store.AccessList
|
|
decodeJSON(t, al, &accessList)
|
|
|
|
strm := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
|
|
"incomingPort": pickFreePort(t),
|
|
"forwardingHost": "127.0.0.1",
|
|
"forwardingPort": 9,
|
|
"tcpForwarding": true,
|
|
"enabled": false,
|
|
}, token)
|
|
if strm.StatusCode != http.StatusCreated {
|
|
t.Fatalf("stream: %d", strm.StatusCode)
|
|
}
|
|
var stream store.Stream
|
|
decodeJSON(t, strm, &stream)
|
|
|
|
redir := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
|
|
"domainNames": []string{"malformed-redir.example"},
|
|
"forwardDomainName": "target.example",
|
|
"enabled": true,
|
|
}, token)
|
|
if redir.StatusCode != http.StatusCreated {
|
|
t.Fatalf("redir: %d", redir.StatusCode)
|
|
}
|
|
var redirection store.RedirectionHost
|
|
decodeJSON(t, redir, &redirection)
|
|
|
|
dead := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
|
|
"domainNames": []string{"malformed-dead.example"},
|
|
"enabled": true,
|
|
}, token)
|
|
if dead.StatusCode != http.StatusCreated {
|
|
t.Fatalf("dead: %d", dead.StatusCode)
|
|
}
|
|
var deadHost store.DeadHost
|
|
decodeJSON(t, dead, &deadHost)
|
|
|
|
putCases := []struct {
|
|
name string
|
|
url string
|
|
}{
|
|
{"proxy-host", env.BaseURL + "/api/proxy-hosts/" + itoa(proxyHost.ID)},
|
|
{"access-list", env.BaseURL + "/api/access-lists/" + itoa(accessList.ID)},
|
|
{"stream", env.BaseURL + "/api/streams/" + itoa(stream.ID)},
|
|
{"redirection", env.BaseURL + "/api/redirection-hosts/" + itoa(redirection.ID)},
|
|
{"dead-host", env.BaseURL + "/api/dead-hosts/" + itoa(deadHost.ID)},
|
|
}
|
|
for _, tc := range putCases {
|
|
t.Run("put "+tc.name, func(t *testing.T) {
|
|
assertStatus(t, apiPutRaw(t, tc.url, `{`, token), http.StatusBadRequest)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPI_UpdateConflict409(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
t.Run("proxy-hosts", func(t *testing.T) {
|
|
a := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
|
|
"domainNames": []string{"conflict-a.example"},
|
|
"forwardHost": "127.0.0.1",
|
|
"forwardPort": 1,
|
|
"forwardScheme": "http",
|
|
}, token)
|
|
b := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
|
|
"domainNames": []string{"conflict-b.example"},
|
|
"forwardHost": "127.0.0.1",
|
|
"forwardPort": 2,
|
|
"forwardScheme": "http",
|
|
}, token)
|
|
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
|
|
}
|
|
var hostB store.ProxyHost
|
|
decodeJSON(t, b, &hostB)
|
|
assertStatus(t, apiPut(t, env.BaseURL+"/api/proxy-hosts/"+itoa(hostB.ID), map[string]any{
|
|
"domainNames": []string{"conflict-a.example"},
|
|
"forwardHost": "127.0.0.1",
|
|
"forwardPort": 2,
|
|
"forwardScheme": "http",
|
|
}, token), http.StatusConflict)
|
|
})
|
|
|
|
t.Run("redirection-hosts", func(t *testing.T) {
|
|
a := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
|
|
"domainNames": []string{"conflict-redir-a.example"},
|
|
"forwardDomainName": "a.example",
|
|
"enabled": true,
|
|
}, token)
|
|
b := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
|
|
"domainNames": []string{"conflict-redir-b.example"},
|
|
"forwardDomainName": "b.example",
|
|
"enabled": true,
|
|
}, token)
|
|
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
|
|
}
|
|
var hostB store.RedirectionHost
|
|
decodeJSON(t, b, &hostB)
|
|
assertStatus(t, apiPut(t, env.BaseURL+"/api/redirection-hosts/"+itoa(hostB.ID), map[string]any{
|
|
"domainNames": []string{"conflict-redir-a.example"},
|
|
"forwardDomainName": "b.example",
|
|
"enabled": true,
|
|
}, token), http.StatusConflict)
|
|
})
|
|
|
|
t.Run("dead-hosts", func(t *testing.T) {
|
|
a := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
|
|
"domainNames": []string{"conflict-dead-a.example"},
|
|
"enabled": true,
|
|
}, token)
|
|
b := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
|
|
"domainNames": []string{"conflict-dead-b.example"},
|
|
"enabled": true,
|
|
}, token)
|
|
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
|
|
}
|
|
var hostB store.DeadHost
|
|
decodeJSON(t, b, &hostB)
|
|
assertStatus(t, apiPut(t, env.BaseURL+"/api/dead-hosts/"+itoa(hostB.ID), map[string]any{
|
|
"domainNames": []string{"conflict-dead-a.example"},
|
|
"enabled": true,
|
|
}, token), http.StatusConflict)
|
|
})
|
|
|
|
t.Run("streams", func(t *testing.T) {
|
|
port := pickFreePort(t)
|
|
a := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
|
|
"incomingPort": port,
|
|
"forwardingHost": "127.0.0.1",
|
|
"forwardingPort": 9,
|
|
"tcpForwarding": true,
|
|
"enabled": false,
|
|
}, token)
|
|
otherPort := pickFreePort(t)
|
|
b := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
|
|
"incomingPort": otherPort,
|
|
"forwardingHost": "127.0.0.1",
|
|
"forwardingPort": 10,
|
|
"tcpForwarding": true,
|
|
"enabled": false,
|
|
}, token)
|
|
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
|
|
}
|
|
var streamB store.Stream
|
|
decodeJSON(t, b, &streamB)
|
|
assertStatus(t, apiPut(t, env.BaseURL+"/api/streams/"+itoa(streamB.ID), map[string]any{
|
|
"incomingPort": port,
|
|
"forwardingHost": "127.0.0.1",
|
|
"forwardingPort": 10,
|
|
"tcpForwarding": true,
|
|
"enabled": false,
|
|
}, token), http.StatusConflict)
|
|
})
|
|
}
|
|
|
|
func TestAPI_ForbiddenEnableDisableBranches(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
adminTok := env.loginToken(t)
|
|
userTok := env.userToken(t, 2)
|
|
|
|
ph := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
|
|
"domainNames": []string{"forbid-enable-ph.example"},
|
|
"forwardHost": "127.0.0.1",
|
|
"forwardPort": 1,
|
|
"forwardScheme": "http",
|
|
"enabled": true,
|
|
}, adminTok)
|
|
var proxyHost store.ProxyHost
|
|
decodeJSON(t, ph, &proxyHost)
|
|
assertStatus(t, apiPost(t, env.BaseURL+"/api/proxy-hosts/"+itoa(proxyHost.ID)+"/enable", nil, userTok), http.StatusForbidden)
|
|
|
|
strm := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
|
|
"incomingPort": pickFreePort(t),
|
|
"forwardingHost": "127.0.0.1",
|
|
"forwardingPort": 9,
|
|
"tcpForwarding": true,
|
|
"enabled": false,
|
|
}, adminTok)
|
|
var stream store.Stream
|
|
decodeJSON(t, strm, &stream)
|
|
assertStatus(t, apiPost(t, env.BaseURL+"/api/streams/"+itoa(stream.ID)+"/disable", nil, userTok), http.StatusForbidden)
|
|
|
|
redir := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
|
|
"domainNames": []string{"forbid-disable-redir.example"},
|
|
"forwardDomainName": "x.example",
|
|
"enabled": true,
|
|
}, adminTok)
|
|
var redirection store.RedirectionHost
|
|
decodeJSON(t, redir, &redirection)
|
|
assertStatus(t, apiPost(t, env.BaseURL+"/api/redirection-hosts/"+itoa(redirection.ID)+"/disable", nil, userTok), http.StatusForbidden)
|
|
|
|
dead := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
|
|
"domainNames": []string{"forbid-enable-dead.example"},
|
|
"enabled": true,
|
|
}, adminTok)
|
|
var deadHost store.DeadHost
|
|
decodeJSON(t, dead, &deadHost)
|
|
assertStatus(t, apiPost(t, env.BaseURL+"/api/dead-hosts/"+itoa(deadHost.ID)+"/enable", nil, userTok), http.StatusForbidden)
|
|
}
|
|
|
|
func TestAPI_LoginLegacyPlainPassword(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
u, ok := env.Store.GetUserByEmail("admin@example.com")
|
|
if !ok {
|
|
t.Fatal("admin user missing")
|
|
}
|
|
u.Password = "legacyplain"
|
|
if err := env.Store.UpdateUser(u); err != nil {
|
|
t.Fatalf("update user: %v", err)
|
|
}
|
|
|
|
resp := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": "legacyplain"}, "")
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("legacy login: %d %s", resp.StatusCode, readBody(resp))
|
|
}
|
|
var loginOut struct {
|
|
Token string `json:"token"`
|
|
}
|
|
decodeJSON(t, resp, &loginOut)
|
|
if loginOut.Token == "" {
|
|
t.Fatal("empty token for legacy login")
|
|
}
|
|
|
|
assertStatus(t, apiPost(t, env.BaseURL+"/api/users/me/password", map[string]any{
|
|
"currentPassword": "legacyplain",
|
|
"newPassword": "newsecure123",
|
|
}, loginOut.Token), http.StatusOK)
|
|
}
|
|
|
|
func TestAPI_PasswordChangeEmptyNew(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
assertStatus(t, apiPost(t, env.BaseURL+"/api/users/me/password", map[string]any{
|
|
"currentPassword": "password",
|
|
"newPassword": "",
|
|
}, token), http.StatusBadRequest)
|
|
}
|
|
|
|
func TestAPI_ListEndpointsOK(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
lists := []string{
|
|
"/api/access-lists",
|
|
"/api/streams",
|
|
"/api/certificates",
|
|
"/api/redirection-hosts",
|
|
"/api/dead-hosts",
|
|
"/api/settings",
|
|
}
|
|
for _, path := range lists {
|
|
t.Run(path, func(t *testing.T) {
|
|
assertStatus(t, apiGet(t, env.BaseURL+path, token), http.StatusOK)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPI_DeleteNotFoundBranches(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
cases := []string{
|
|
"/api/redirection-hosts/9999",
|
|
"/api/dead-hosts/9999",
|
|
"/api/streams/9999",
|
|
}
|
|
for _, path := range cases {
|
|
t.Run(path, func(t *testing.T) {
|
|
assertStatus(t, apiDelete(t, env.BaseURL+path, token), http.StatusNotFound)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPI_PutNotFoundBranches(t *testing.T) {
|
|
env := newAPITestServer(t)
|
|
token := env.loginToken(t)
|
|
|
|
cases := []struct {
|
|
path string
|
|
body map[string]any
|
|
}{
|
|
{"/api/streams/9999", map[string]any{
|
|
"incomingPort": 1, "forwardingHost": "127.0.0.1", "forwardingPort": 1, "tcpForwarding": true,
|
|
}},
|
|
{"/api/redirection-hosts/9999", map[string]any{"domainNames": []string{"x.example"}}},
|
|
{"/api/dead-hosts/9999", map[string]any{"domainNames": []string{"x.example"}}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.path, func(t *testing.T) {
|
|
assertStatus(t, apiPut(t, env.BaseURL+tc.path, tc.body, token), http.StatusNotFound)
|
|
})
|
|
}
|
|
}
|