Files
Dušan Borovčanin 168e8b90cb
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
NOISSUE - Move rules engine, alarms, reports, journal and notifications to EE (#3552)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-07-30 17:55:57 +02:00

547 lines
18 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package atom
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestUpsertResourceCreatesThenUpdatesOnConflict(t *testing.T) {
var operations []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
payload := string(body)
switch {
case strings.Contains(payload, "createResource"):
operations = append(operations, "createResource")
_ = json.NewEncoder(w).Encode(map[string]any{
"errors": []map[string]string{{"message": "duplicate key value violates unique constraint"}},
})
return
case strings.Contains(payload, "updateResource"):
operations = append(operations, "updateResource")
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"updateResource": map[string]any{"id": "res-1", "kind": KindChannel, "name": "ch"},
},
})
return
default:
t.Fatalf("unexpected GraphQL payload: %s", payload)
}
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
if err := client.UpsertResource(context.Background(), Resource{ID: "res-1", Kind: KindChannel, Name: "ch"}); err != nil {
t.Fatalf("upsert failed: %v", err)
}
want := []string{"createResource", "updateResource"}
if len(operations) != len(want) {
t.Fatalf("unexpected operation count: got %v want %v", operations, want)
}
for i := range want {
if operations[i] != want[i] {
t.Fatalf("unexpected operations: got %v want %v", operations, want)
}
}
}
func TestListResources(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if payload.Variables["kind"] != KindRule || payload.Variables["tenantId"] != testDomainID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"resources": map[string]any{
"items": []Resource{{ID: "rule-1", Kind: KindRule, Name: "high-temp"}},
"total": 1,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.ListResources(context.Background(), Query{Kind: KindRule, TenantID: testDomainID})
if err != nil {
t.Fatalf("list failed: %v", err)
}
if got.Total != 1 || got.Items[0].ID != "rule-1" {
t.Fatalf("unexpected list: %+v", got)
}
}
func TestCurrentAtomCompatibilitySurface(t *testing.T) {
const (
serviceToken = "service-token"
runtimeToken = "runtime-token"
)
seen := map[string]bool{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case atomGraphQLPath:
if r.Method != http.MethodPost {
t.Fatalf("unexpected GraphQL method: %s", r.Method)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+serviceToken {
t.Fatalf("unexpected GraphQL authorization header: %q", got)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
switch {
case strings.Contains(payload.Query, "authzCheck"):
seen["authzCheck"] = true
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected authz input: %+v", payload.Variables["input"])
}
if input["subjectId"] != testEntityID || input["action"] != atomActionPublish || input["resourceId"] != "channel-1" {
t.Fatalf("unexpected authz input: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"authzCheck": map[string]any{"allowed": true, "reason": "allowed"},
},
})
case strings.Contains(payload.Query, "authorizedObjectIds"):
seen["authorizedObjectIds"] = true
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected authorized objects input: %+v", payload.Variables["input"])
}
if input["subjectId"] != testEntityID ||
input["action"] != atomActionRead ||
input["objectKind"] != atomObjectKindEntity ||
input["objectType"] != atomKindDevice ||
input["tenantId"] != testDomainID {
t.Fatalf("unexpected authorized objects input: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"authorizedObjectIds": map[string]any{"ids": []string{testDeviceID}, "total": 1},
},
})
case strings.Contains(payload.Query, "createAccessToken"):
seen["createAccessToken"] = true
input, ok := payload.Variables["input"].(map[string]any)
if !ok ||
input["name"] != "magistrala-service" ||
input["description"] != "Magistrala service token" ||
input["subjectId"] != testEntityID ||
input["scoped"] != false ||
len(input["permissions"].([]any)) != 0 {
t.Fatalf("unexpected createAccessToken input: %+v", payload.Variables["input"])
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createAccessToken": map[string]any{
"credentialId": testCredentialID,
"token": "atom_00000000000000000000000000000000_0000000000000000000000000000000000000000000000000000000000000000",
"name": "magistrala-service",
},
},
})
default:
t.Fatalf("unexpected GraphQL payload: %s", payload.Query)
}
case atomAuthIntrospectPath:
seen["introspect"] = true
if r.Method != http.MethodGet {
t.Fatalf("unexpected introspection method: %s", r.Method)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+runtimeToken {
t.Fatalf("unexpected introspection authorization header: %q", got)
}
_ = json.NewEncoder(w).Encode(IntrospectionResponse{
Active: true,
EntityID: testEntityID,
TenantID: testDomainID,
SessionID: "session-1",
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Token: serviceToken, Timeout: time.Second})
authz, err := client.CheckAuthz(context.Background(), AuthzRequest{
SubjectID: testEntityID,
Action: atomActionPublish,
ResourceID: "channel-1",
})
if err != nil {
t.Fatalf("authz check failed: %v", err)
}
if !authz.Allowed {
t.Fatalf("unexpected authz response: %+v", authz)
}
objects, err := client.AuthorizedObjectIDs(context.Background(), AuthorizedObjectIDsQuery{
SubjectID: testEntityID,
Action: atomActionRead,
ObjectKind: atomObjectKindEntity,
ObjectType: atomKindDevice,
TenantID: testDomainID,
Limit: 10,
})
if err != nil {
t.Fatalf("authorized object listing failed: %v", err)
}
if objects.Total != 1 || len(objects.IDs) != 1 || objects.IDs[0] != testDeviceID {
t.Fatalf("unexpected authorized object listing: %+v", objects)
}
created, err := client.CreateUnscopedAccessToken(context.Background(), testEntityID, "magistrala-service", "Magistrala service token")
if err != nil {
t.Fatalf("create access token failed: %v", err)
}
if created.CredentialID != testCredentialID || created.Token == "" {
t.Fatalf("unexpected access token response: %+v", created)
}
introspection, err := client.Introspect(context.Background(), runtimeToken)
if err != nil {
t.Fatalf("introspection failed: %v", err)
}
if !introspection.Active || introspection.EntityID != testEntityID || introspection.TenantID != testDomainID {
t.Fatalf("unexpected introspection response: %+v", introspection)
}
for _, operation := range []string{"authzCheck", "authorizedObjectIds", "createAccessToken", "introspect"} {
if !seen[operation] {
t.Fatalf("operation %q was not exercised", operation)
}
}
}
func TestCreateTenantMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "createTenant") || !strings.Contains(payload.Query, "route: alias") {
t.Fatalf("query does not map tenant alias to route: %s", payload.Query)
}
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected input: %+v", payload.Variables["input"])
}
if input["alias"] != "d1" {
t.Fatalf("expected alias input from route, got: %+v", input)
}
if _, ok := input["route"]; ok {
t.Fatalf("input must not use Atom route field: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createTenant": map[string]any{
"id": testTenantID,
"name": "D1",
"route": "d1",
"status": "active",
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.CreateTenant(context.Background(), Tenant{Name: "D1", Route: "d1"})
if err != nil {
t.Fatalf("create tenant failed: %v", err)
}
if got.ID != testTenantID || got.Route != "d1" {
t.Fatalf("unexpected tenant: %+v", got)
}
}
func TestUpdateTenantMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "updateTenant") || !strings.Contains(payload.Query, "route: alias") {
t.Fatalf("query does not map tenant alias to route: %s", payload.Query)
}
if payload.Variables["id"] != testTenantID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected input: %+v", payload.Variables["input"])
}
if input["alias"] != "d1" {
t.Fatalf("expected alias input from route, got: %+v", input)
}
if _, ok := input["route"]; ok {
t.Fatalf("input must not use Atom route field: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"updateTenant": map[string]any{
"id": testTenantID,
"name": "D1",
"route": "d1",
"status": "active",
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.UpdateTenant(context.Background(), testTenantID, Tenant{Name: "D1", Route: "d1"})
if err != nil {
t.Fatalf("update tenant failed: %v", err)
}
if got.ID != testTenantID || got.Route != "d1" {
t.Fatalf("unexpected tenant: %+v", got)
}
}
func TestListTenantsMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "$alias: String") ||
!strings.Contains(payload.Query, "alias: $alias") ||
!strings.Contains(payload.Query, "route: alias") {
t.Fatalf("query does not use Atom alias for tenant route lookup: %s", payload.Query)
}
if strings.Contains(payload.Query, "$route") || strings.Contains(payload.Query, "route: $route") {
t.Fatalf("query must not use removed Atom route field/filter: %s", payload.Query)
}
if payload.Variables["alias"] != "d1" {
t.Fatalf("expected alias variable from route, got: %+v", payload.Variables)
}
if _, ok := payload.Variables["route"]; ok {
t.Fatalf("variables must not use Atom route field: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"tenants": map[string]any{
"items": []Tenant{{ID: testTenantID, Name: "D1", Route: "d1", Status: "active"}},
"total": 1,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.ListTenants(context.Background(), Query{Route: "d1", Limit: 1})
if err != nil {
t.Fatalf("list tenants failed: %v", err)
}
if got.Total != 1 || len(got.Items) != 1 || got.Items[0].Route != "d1" {
t.Fatalf("unexpected tenants: %+v", got)
}
}
func TestCreateSharedKey(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "createSharedKey") {
t.Fatalf("query does not create shared key: %s", payload.Query)
}
if payload.Variables["entityId"] != testDeviceID {
t.Fatalf("unexpected entity id: %+v", payload.Variables)
}
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected input: %+v", payload.Variables["input"])
}
if input["key"] != testClientKey || input["description"] != "provisioned from mg" {
t.Fatalf("unexpected input: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createSharedKey": map[string]any{
"credentialId": testCredentialID,
"key": testClientKey,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.CreateSharedKey(context.Background(), testDeviceID, testClientKey, "provisioned from mg")
if err != nil {
t.Fatalf("create shared key failed: %v", err)
}
if got.CredentialID != testCredentialID || got.Key != testClientKey {
t.Fatalf("unexpected shared key response: %+v", got)
}
}
func TestRevealSharedKey(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "revealSharedKey") {
t.Fatalf("query does not reveal shared key: %s", payload.Query)
}
if payload.Variables["entityId"] != testDeviceID || payload.Variables["credentialId"] != testCredentialID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"revealSharedKey": map[string]any{
"credentialId": testCredentialID,
"key": testClientKey,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.RevealSharedKey(context.Background(), testDeviceID, testCredentialID)
if err != nil {
t.Fatalf("reveal shared key failed: %v", err)
}
if got.CredentialID != testCredentialID || got.Key != testClientKey {
t.Fatalf("unexpected shared key response: %+v", got)
}
}
func TestListCredentials(t *testing.T) {
createdAt := "2026-06-30T10:15:30Z"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "credentials") {
t.Fatalf("query does not list credentials: %s", payload.Query)
}
if payload.Variables["entityId"] != testDeviceID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"credentials": map[string]any{
"total": 1,
"items": []map[string]any{{
"id": testCredentialID,
"entity_id": testDeviceID,
"kind": "shared_key",
"identifier": "",
"status": "active",
"created_at": createdAt,
}},
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.ListCredentials(context.Background(), testDeviceID)
if err != nil {
t.Fatalf("list credentials failed: %v", err)
}
if got.Total != 1 || len(got.Items) != 1 {
t.Fatalf("unexpected credentials response: %+v", got)
}
item := got.Items[0]
if item.ID != testCredentialID || item.EntityID != testDeviceID || item.Kind != "shared_key" || item.Status != "active" {
t.Fatalf("unexpected credential item: %+v", item)
}
if item.CreatedAt.Format(time.RFC3339) != createdAt {
t.Fatalf("unexpected created_at: %s", item.CreatedAt.Format(time.RFC3339))
}
}
func TestLoadConfig(t *testing.T) {
t.Setenv("ATOM_URL", "http://atom:8080/")
t.Setenv("ATOM_ADMIN_TOKEN", "token")
t.Setenv("ATOM_ADMIN_USERNAME", "admin")
t.Setenv("ATOM_ADMIN_SECRET", "secret")
t.Setenv("ATOM_TIMEOUT", "3s")
cfg := LoadConfig()
if cfg.URL != "http://atom:8080" || cfg.JWKSURL != "http://atom:8080/.well-known/jwks.json" || cfg.Token != "token" || cfg.AdminUsername != "admin" || cfg.AdminSecret != "secret" {
t.Fatalf("unexpected config: %+v", cfg)
}
if cfg.Timeout != 3*time.Second {
t.Fatalf("unexpected timeout: %s", cfg.Timeout)
}
}