节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
6.76 KiB
Stardust
package protocol

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"testing"
	"time"
)

type basicVectors struct {
	LoginRequest  json.RawMessage `json:"loginRequest"`
	LoginResponse json.RawMessage `json:"loginResponse"`
	PingRequest   json.RawMessage `json:"pingRequest"`
	PingResponse  json.RawMessage `json:"pingResponse"`
	CommandReply  json.RawMessage `json:"commandReply"`
	Event         json.RawMessage `json:"event"`
	UpgradeInfo   json.RawMessage `json:"upgradeInfo"`
}

func loadVectors(t *testing.T) basicVectors {
	t.Helper()

	path := filepath.Join("..", "..", "..", "spec", "vectors", "basic.json")
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read vectors: %v", err)
	}

	var vectors basicVectors
	if err := json.Unmarshal(data, &vectors); err != nil {
		t.Fatalf("decode vectors: %v", err)
	}

	return vectors
}

func TestBasicVectors(t *testing.T) {
	vectors := loadVectors(t)

	var login LoginInfo
	if err := Unmarshal(vectors.LoginRequest, &login); err != nil {
		t.Fatalf("login request: %v", err)
	}
	if login.ProductCode != "StarAgent-Go" || login.Node.MachineName != "web-01" {
		t.Fatalf("unexpected login request: %+v", login)
	}
	if login.Node.Compile.String() != "2026-10-17 08:00:00" {
		t.Fatalf("unexpected compile: %s", login.Node.Compile)
	}
	if len(login.Node.Plugins) != 4 {
		t.Fatalf("unexpected plugins: %v", login.Node.Plugins)
	}

	var loginResp LoginResponse
	if err := Unmarshal(vectors.LoginResponse, &loginResp); err != nil {
		t.Fatalf("login response: %v", err)
	}
	if loginResp.Token != "jwt-token" || loginResp.Expire != 7200 || loginResp.Code != "A1B2C3D4" {
		t.Fatalf("unexpected login response: %+v", loginResp)
	}

	var ping PingInfo
	if err := Unmarshal(vectors.PingRequest, &ping); err != nil {
		t.Fatalf("ping request: %v", err)
	}
	if ping.Time != 1760000000000 || ping.CpuRate != 12.5 || ping.ProcessCount != 120 {
		t.Fatalf("unexpected ping request: %+v", ping)
	}

	var pingResp PingResponse
	if err := Unmarshal(vectors.PingResponse, &pingResp); err != nil {
		t.Fatalf("ping response: %v", err)
	}
	if pingResp.Period != 60 || len(pingResp.Commands) != 1 {
		t.Fatalf("unexpected ping response: %+v", pingResp)
	}
	cmd := pingResp.Commands[0]
	if cmd.Id != 123 || cmd.Command != "bash" || cmd.Status != StatusReady {
		t.Fatalf("unexpected command: %+v", cmd)
	}
	if cmd.StartTime.IsSet() || cmd.Expire.IsSet() {
		t.Fatalf("zero command times should not be set: %+v", cmd)
	}

	var reply CommandReplyModel
	if err := Unmarshal(vectors.CommandReply, &reply); err != nil {
		t.Fatalf("command reply: %v", err)
	}
	if reply.Id != 123 || reply.Status != StatusCompleted || reply.Data == "" {
		t.Fatalf("unexpected reply: %+v", reply)
	}

	var event EventModel
	if err := Unmarshal(vectors.Event, &event); err != nil {
		t.Fatalf("event: %v", err)
	}
	if event.Type != "info" || event.Name != "Startup" {
		t.Fatalf("unexpected event: %+v", event)
	}

	var upgrade UpgradeInfo
	if err := Unmarshal(vectors.UpgradeInfo, &upgrade); err != nil {
		t.Fatalf("upgrade: %v", err)
	}
	if upgrade.Version != "1.1.2026.1101" || upgrade.FileSize != 12345678 {
		t.Fatalf("unexpected upgrade: %+v", upgrade)
	}
}

func TestTimeFormats(t *testing.T) {
	cases := []struct {
		input string
		want  time.Time
	}{
		{`"2026-10-17 10:00:00"`, time.Date(2026, 10, 17, 10, 0, 0, 0, time.UTC)},
		{`"2026-10-17T10:00:00Z"`, time.Date(2026, 10, 17, 10, 0, 0, 0, time.UTC)},
		{`"2026-10-17T10:00:00.1234567Z"`, time.Date(2026, 10, 17, 10, 0, 0, 123456700, time.UTC)},
		{`"0001-01-01 00:00:00"`, time.Time{}},
		{`null`, time.Time{}},
		{`""`, time.Time{}},
	}

	for _, c := range cases {
		var got Time
		if err := json.Unmarshal([]byte(c.input), &got); err != nil {
			t.Fatalf("unmarshal %s: %v", c.input, err)
		}
		if !got.Equal(c.want) {
			t.Fatalf("unmarshal %s = %v, want %v", c.input, got.Time, c.want)
		}
	}

	var invalid Time
	if err := json.Unmarshal([]byte(`"not-a-time"`), &invalid); err == nil {
		t.Fatal("expected invalid time error")
	}
}

func TestTimeMarshalUsesUTC(t *testing.T) {
	local := time.Date(2026, 10, 17, 18, 0, 0, 0, time.FixedZone("CST", 8*3600))
	got, err := json.Marshal(NewTime(local))
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	if string(got) != `"2026-10-17 10:00:00"` {
		t.Fatalf("unexpected marshal: %s", got)
	}

	got, err = json.Marshal(Time{})
	if err != nil {
		t.Fatalf("marshal zero: %v", err)
	}
	if string(got) != `"0001-01-01 00:00:00"` {
		t.Fatalf("unexpected zero marshal: %s", got)
	}
}

func TestUnknownAndMissingFields(t *testing.T) {
	var resp PingResponse
	data := []byte(`{"Period":30,"UnknownFutureField":{"nested":true},"Token":null,"Commands":null}`)
	if err := Unmarshal(data, &resp); err != nil {
		t.Fatalf("unknown fields should be ignored: %v", err)
	}
	if resp.Period != 30 || resp.Token != "" || len(resp.Commands) != 0 {
		t.Fatalf("unexpected response: %+v", resp)
	}

	var empty LoginResponse
	if err := Unmarshal([]byte(`{}`), &empty); err != nil {
		t.Fatalf("empty object: %v", err)
	}
	if empty.Expire != 0 || empty.ServerTime != 0 {
		t.Fatalf("missing numbers should be zero: %+v", empty)
	}
}

func TestCaseInsensitiveFields(t *testing.T) {
	var resp LoginResponse
	if err := Unmarshal([]byte(`{"code":"C1","TOKEN":"t1","expire":60,"servertime":99}`), &resp); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if resp.Code != "C1" || resp.Token != "t1" || resp.Expire != 60 || resp.ServerTime != 99 {
		t.Fatalf("case-insensitive decode failed: %+v", resp)
	}
}

func TestParseWrappedResponse(t *testing.T) {
	var resp LoginResponse
	if err := ParseResponse([]byte(`{"code":0,"data":{"Token":"abc","Expire":120},"msg":"ok"}`), &resp); err != nil {
		t.Fatalf("wrapped: %v", err)
	}
	if resp.Token != "abc" || resp.Expire != 120 {
		t.Fatalf("wrapped decode failed: %+v", resp)
	}

	if err := ParseResponse([]byte(`{"Token":"bare"}`), &resp); err != nil {
		t.Fatalf("bare: %v", err)
	}
	if resp.Token != "bare" {
		t.Fatalf("bare decode failed: %+v", resp)
	}

	var apiErr *APIError
	err := ParseResponse([]byte(`{"code":5,"data":null,"msg":"denied"}`), &resp)
	if !errors.As(err, &apiErr) || apiErr.Code != 5 || apiErr.Msg != "denied" {
		t.Fatalf("expected APIError, got %v", err)
	}
}

func TestParseCount(t *testing.T) {
	cases := map[string]int{
		`3`:                            3,
		`"4"`:                          4,
		`{"code":0,"data":5,"msg":""}`: 5,
		`{"code":0,"data":"6"}`:        6,
	}
	for input, want := range cases {
		got, err := ParseCount([]byte(input))
		if err != nil {
			t.Fatalf("ParseCount(%s): %v", input, err)
		}
		if got != want {
			t.Fatalf("ParseCount(%s) = %d, want %d", input, got, want)
		}
	}

	if _, err := ParseCount([]byte(`{"code":7,"msg":"bad"}`)); err == nil {
		t.Fatal("expected error for non-zero code")
	}
	if _, err := ParseCount([]byte(`[]`)); err == nil {
		t.Fatal("expected error for unsupported payload")
	}
}