package main import ( "bytes" "context" "encoding/json" "io" "net/http" "net/http/cookiejar" "strings" "testing" "time" "git.asio.asia/nixevol/NixMsg/internal/config" ) // TestUplinkDisableFatalAndRevoked 验证停用在线端收到 fatal,已推送投递收到 revoked。 func TestUplinkDisableFatalAndRevoked(t *testing.T) { dataDir := t.TempDir() cfgPath := writeTestConfig(t, dataDir) initAdminForTest(t, dataDir) enableRegistration(t, dataDir, "uplink-code") cfg, err := config.Load(cfgPath) if err != nil { t.Fatal(err) } if vErr := cfg.Validate(); vErr != nil { t.Fatal(vErr) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() errCh := make(chan error, 1) go func() { errCh <- runServe(ctx, cfg) }() defer func() { cancel() select { case err := <-errCh: if err != nil { t.Errorf("serve exit: %v", err) } case <-time.After(15 * time.Second): t.Error("serve did not stop") } }() addr := waitListenAddr(t, dataDir, 15*time.Second) base := "http://" + addr registerEP(t, base, "alice", "password12", "Alice") registerEP(t, base, "bob", "password12", "Bob") alice := mqttSessionLogin(t, base, "alice", "password12") defer alice.Close() bob := mqttSessionLogin(t, base, "bob", "password12") defer bob.Close() delay0 := int64(0) sendResp := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "s1", "id": "dm-fatal-1", "to": map[string]any{"kind": "endpoint", "id": "bob"}, "body": map[string]any{"enc": "utf8", "data": "to-void"}, "delay_ms": delay0, }) if !sendResp.OK { t.Fatalf("send: %+v", sendResp) } msg := bob.WaitType(t, "msg", 8*time.Second) if msg["id"] != "dm-fatal-1" { t.Fatalf("bob msg=%v", msg) } admin := adminHTTPClient(t, base) disableEP(t, admin, base, "bob") fatal := bob.WaitType(t, "fatal", 8*time.Second) if fatal["reason"] != "disabled" { t.Fatalf("fatal=%v", fatal) } revoked := bob.WaitType(t, "revoked", 8*time.Second) if revoked["id"] != "dm-fatal-1" || revoked["reason"] != "endpoint_disabled" { t.Fatalf("revoked=%v", revoked) } } // TestUplinkResetPasswordFatal 验证重置登录密码后在线端收到 fatal(password_reset)。 func TestUplinkResetPasswordFatal(t *testing.T) { dataDir := t.TempDir() cfgPath := writeTestConfig(t, dataDir) initAdminForTest(t, dataDir) enableRegistration(t, dataDir, "uplink-code") cfg, err := config.Load(cfgPath) if err != nil { t.Fatal(err) } if vErr := cfg.Validate(); vErr != nil { t.Fatal(vErr) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() errCh := make(chan error, 1) go func() { errCh <- runServe(ctx, cfg) }() defer func() { cancel() select { case err := <-errCh: if err != nil { t.Errorf("serve exit: %v", err) } case <-time.After(15 * time.Second): t.Error("serve did not stop") } }() addr := waitListenAddr(t, dataDir, 15*time.Second) base := "http://" + addr registerEP(t, base, "carol", "password12", "Carol") carol := mqttSessionLogin(t, base, "carol", "password12") defer carol.Close() admin := adminHTTPClient(t, base) resetLoginPassword(t, admin, base, "carol", "password99xx") fatal := carol.WaitType(t, "fatal", 8*time.Second) if fatal["reason"] != "password_reset" { t.Fatalf("fatal=%v", fatal) } } func adminHTTPClient(t *testing.T, base string) *http.Client { t.Helper() jar, err := cookiejar.New(nil) if err != nil { t.Fatal(err) } client := &http.Client{Jar: jar, Timeout: 10 * time.Second} loginBody, _ := json.Marshal(map[string]string{ "username": "admin", "password": "test-admin-password-xx", }) resp, err := client.Post(base+"/api/admin/login", "application/json", bytes.NewReader(loginBody)) if err != nil { t.Fatal(err) } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("admin login: %d %s", resp.StatusCode, raw) } return client } func disableEP(t *testing.T, client *http.Client, base, id string) { t.Helper() req, err := http.NewRequest(http.MethodPatch, base+"/api/admin/endpoints/"+id, strings.NewReader(`{"enabled":false}`)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Nixmsg-Request", "1") resp, err := client.Do(req) if err != nil { t.Fatal(err) } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("disable %s: %d %s", id, resp.StatusCode, raw) } } func resetLoginPassword(t *testing.T, client *http.Client, base, id, password string) { t.Helper() body := `{"login_password":"` + password + `"}` req, err := http.NewRequest(http.MethodPost, base+"/api/admin/endpoints/"+id+"/reset-login-password", strings.NewReader(body)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Nixmsg-Request", "1") resp, err := client.Do(req) if err != nil { t.Fatal(err) } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("reset password %s: %d %s", id, resp.StatusCode, raw) } }