package api_test import ( "context" "encoding/json" "net/http" "testing" "git.ryuvia.com/niklas/terdut-server/internal/api" ) const ( operatorKeyOne = "tdsa_operator-key-number-one-0123456789" operatorKeyTwo = "tdsa_operator-key-number-two-0123456789" ) func TestSeedOperatorKey_AuthenticatesAsInstanceAccount(t *testing.T) { s := newTS(t) if err := api.SeedOperatorKey(context.Background(), s.db, operatorKeyOne); err != nil { t.Fatal(err) } resp := s.reqAs(t, operatorKeyOne, http.MethodPost, "/api/teams", map[string]string{"name": "seeded"}) resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("seeded key should create a team, got %d", resp.StatusCode) } } func TestSeedOperatorKey_RotationReplacesAndIsIdempotent(t *testing.T) { s := newTS(t) ctx := context.Background() for _, key := range []string{operatorKeyOne, operatorKeyOne, operatorKeyTwo} { if err := api.SeedOperatorKey(ctx, s.db, key); err != nil { t.Fatal(err) } } old := s.reqAs(t, operatorKeyOne, http.MethodGet, "/api/teams", nil) old.Body.Close() if old.StatusCode != http.StatusUnauthorized { t.Errorf("rotated-out key should be refused, got %d", old.StatusCode) } cur := s.reqAs(t, operatorKeyTwo, http.MethodGet, "/api/teams", nil) cur.Body.Close() if cur.StatusCode != http.StatusOK { t.Errorf("current key should work, got %d", cur.StatusCode) } var accounts, keys int s.db.QueryRow("SELECT COUNT(*) FROM service_accounts WHERE name = 'terdut-operator'").Scan(&accounts) s.db.QueryRow("SELECT COUNT(*) FROM service_account_keys").Scan(&keys) if accounts != 1 || keys != 1 { t.Errorf("expected one account and one key, got %d and %d", accounts, keys) } } func TestSeedOperatorKey_EmptyKeyDoesNothing(t *testing.T) { s := newTS(t) if err := api.SeedOperatorKey(context.Background(), s.db, ""); err != nil { t.Fatal(err) } var n int s.db.QueryRow("SELECT COUNT(*) FROM service_accounts").Scan(&n) if n != 0 { t.Errorf("expected no service account, got %d", n) } } // The instance account configures a team it did not create, which is what lets // the operator hold one credential instead of one per team, yet it is not a // member and so reads none of the team's incidents. func TestInstanceAccount_ActsAsOwnerOfAnyTeamButIsNoMember(t *testing.T) { s := newTS(t) other := newTeam(t, s, "other") if err := api.SeedOperatorKey(context.Background(), s.db, operatorKeyOne); err != nil { t.Fatal(err) } rename := s.reqAs(t, operatorKeyOne, http.MethodPut, "/api/teams/"+id64(other.id), map[string]string{"name": "renamed"}) rename.Body.Close() if rename.StatusCode >= 300 { t.Errorf("instance account should rename any team, got %d", rename.StatusCode) } // Not a member: the team's queue is not visible to it. var queue []map[string]any decode(t, s.reqAs(t, operatorKeyOne, http.MethodGet, "/api/incidents", nil), &queue) if len(queue) != 0 { t.Errorf("instance account should see no incidents, got %v", queue) } } // external_id lets automation find its own team again after a crash, without // trusting a display name. func TestCreateTeam_ExternalIDIsIdempotentAndInstanceOnly(t *testing.T) { s := newTS(t) if err := api.SeedOperatorKey(context.Background(), s.db, operatorKeyOne); err != nil { t.Fatal(err) } create := func(name string) (int, map[string]any) { resp := s.reqAs(t, operatorKeyOne, http.MethodPost, "/api/teams", map[string]string{"name": name, "external_id": "ns/platform"}) var out map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) resp.Body.Close() return resp.StatusCode, out } code, first := create("Platform") if code != http.StatusCreated { t.Fatalf("first create: %d", code) } // Same identity, even under a new display name: the same team comes back. code, again := create("Platform renamed") if code != http.StatusOK || again["id"] != first["id"] { t.Errorf("repeat with the same external_id: want 200 and team %v, got %d %v", first["id"], code, again) } // A different identity cannot take the name. resp := s.reqAs(t, operatorKeyOne, http.MethodPost, "/api/teams", map[string]string{"name": "Platform", "external_id": "other/platform"}) resp.Body.Close() if resp.StatusCode != http.StatusConflict { t.Errorf("taken name under another external_id: want 409, got %d", resp.StatusCode) } // A person cannot set one. resp = s.req(t, http.MethodPost, "/api/teams", map[string]string{"name": "Mine", "external_id": "x/y"}) resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Errorf("a user setting external_id: want 403, got %d", resp.StatusCode) } }