Files
go-nc-exapp/notifications_test.go
Konrad NeitzelandCursor fc2f9f63c2 Add AppAPI Notifications and Users and Groups reads.
ExApps can Send/SendTo a bell for one Recipient and read group membership and directory OCS as the Requesting user, with Access Gate using UserGroups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 14:44:42 +02:00

269 lines
8.0 KiB
Go

package gonexapp_test
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.neitzel.de/konrad/go-nc-exapp"
)
func authUserFromRequest(r *http.Request) string {
raw := r.Header.Get("AUTHORIZATION-APP-API")
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return ""
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) < 1 {
return ""
}
return parts[0]
}
func TestAppAPINotificationsSendPostsForCredentialsUser(t *testing.T) {
var (
gotMethod string
gotPath string
gotUser string
gotBody []byte
)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
gotPath = r.URL.Path
gotUser = authUserFromRequest(r)
gotBody, _ = io.ReadAll(r.Body)
_ = json.NewEncoder(w).Encode(map[string]any{"ocs": map[string]any{"data": map[string]any{}}})
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
err := api.Send(gonexapp.Notification{Subject: "Hello"})
if err != nil {
t.Fatal(err)
}
if gotMethod != http.MethodPost {
t.Fatalf("method=%q", gotMethod)
}
if !strings.Contains(gotPath, "apps/app_api/api/v1/notification") {
t.Fatalf("path=%q", gotPath)
}
if gotUser != "alice" {
t.Fatalf("auth user=%q", gotUser)
}
var payload struct {
Params struct {
Object string `json:"object"`
ObjectID string `json:"object_id"`
SubjectType string `json:"subject_type"`
SubjectParams struct {
RichSubject string `json:"rich_subject"`
RichSubjectParams map[string]any `json:"rich_subject_params"`
} `json:"subject_params"`
} `json:"params"`
}
if err := json.Unmarshal(gotBody, &payload); err != nil {
t.Fatalf("body: %v\n%s", err, gotBody)
}
if payload.Params.Object != "app_api" {
t.Fatalf("object=%q", payload.Params.Object)
}
if payload.Params.ObjectID != "app_api_id" {
t.Fatalf("object_id=%q", payload.Params.ObjectID)
}
if payload.Params.SubjectType != "app_api_ex_app" {
t.Fatalf("subject_type=%q", payload.Params.SubjectType)
}
if payload.Params.SubjectParams.RichSubject != "Hello" {
t.Fatalf("rich_subject=%q", payload.Params.SubjectParams.RichSubject)
}
if payload.Params.SubjectParams.RichSubjectParams == nil {
t.Fatal("rich_subject_params is null")
}
if len(payload.Params.SubjectParams.RichSubjectParams) != 0 {
t.Fatalf("rich_subject_params=%#v", payload.Params.SubjectParams.RichSubjectParams)
}
}
func TestAppAPINotificationsSendToImpersonatesGivenUser(t *testing.T) {
var gotUser string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotUser = authUserFromRequest(r)
_ = json.NewEncoder(w).Encode(map[string]any{"ocs": map[string]any{"data": map[string]any{}}})
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
err := api.SendTo("bob", gonexapp.Notification{Subject: "Hello"})
if err != nil {
t.Fatal(err)
}
if gotUser != "bob" {
t.Fatalf("auth user=%q, want bob", gotUser)
}
}
func TestAppAPINotificationsRejectsEmptySubject(t *testing.T) {
called := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
_ = json.NewEncoder(w).Encode(map[string]any{"ocs": map[string]any{"data": map[string]any{}}})
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
if err := api.Send(gonexapp.Notification{}); err == nil {
t.Fatal("Send: expected error for empty Subject")
}
if err := api.SendTo("bob", gonexapp.Notification{Message: "no subject"}); err == nil {
t.Fatal("SendTo: expected error for empty Subject")
}
if called {
t.Fatal("OCS was called")
}
}
func TestAppAPINotificationsSendRejectsEmptyUserID(t *testing.T) {
called := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
_ = json.NewEncoder(w).Encode(map[string]any{"ocs": map[string]any{"data": map[string]any{}}})
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
if err := api.Send(gonexapp.Notification{Subject: "Hello"}); err == nil {
t.Fatal("expected error for empty UserID")
}
if called {
t.Fatal("OCS was called")
}
}
func TestAppAPINotificationsSendToRejectsEmptyUserID(t *testing.T) {
called := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
_ = json.NewEncoder(w).Encode(map[string]any{"ocs": map[string]any{"data": map[string]any{}}})
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
if err := api.SendTo("", gonexapp.Notification{Subject: "Hello"}); err == nil {
t.Fatal("expected error for empty userID")
}
if called {
t.Fatal("OCS was called")
}
}
func TestAppAPINotificationsSendIncludesOptionalMessageAndLink(t *testing.T) {
var gotBody []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotBody, _ = io.ReadAll(r.Body)
_ = json.NewEncoder(w).Encode(map[string]any{"ocs": map[string]any{"data": map[string]any{}}})
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
err := api.Send(gonexapp.Notification{
Subject: "Hello",
Message: "Details here",
Link: "https://cloud.example/apps/checkdns",
})
if err != nil {
t.Fatal(err)
}
var payload struct {
Params struct {
SubjectParams struct {
RichMessage string `json:"rich_message"`
RichMessageParams map[string]any `json:"rich_message_params"`
Link string `json:"link"`
} `json:"subject_params"`
} `json:"params"`
}
if err := json.Unmarshal(gotBody, &payload); err != nil {
t.Fatalf("body: %v\n%s", err, gotBody)
}
if payload.Params.SubjectParams.RichMessage != "Details here" {
t.Fatalf("rich_message=%q", payload.Params.SubjectParams.RichMessage)
}
if payload.Params.SubjectParams.RichMessageParams == nil {
t.Fatal("rich_message_params is null")
}
if payload.Params.SubjectParams.Link != "https://cloud.example/apps/checkdns" {
t.Fatalf("link=%q", payload.Params.SubjectParams.Link)
}
}
func TestAppAPINotificationsSendSurfacesOCSStatus(t *testing.T) {
for _, code := range []int{http.StatusBadRequest, http.StatusForbidden} {
t.Run(http.StatusText(code), func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nope", code)
}))
t.Cleanup(srv.Close)
cred := gonexapp.Credentials{
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
}
api := gonexapp.NewAppAPINotifications(cred)
api.Client = srv.Client()
api.OCS.Client = srv.Client()
err := api.Send(gonexapp.Notification{Subject: "Hello"})
if err == nil {
t.Fatal("expected error")
}
want := fmt.Sprintf("%d", code)
if !strings.Contains(err.Error(), want) {
t.Fatalf("error %q missing status %s", err, want)
}
})
}
}